sipeed/picoclaw · warning
${label}.${key} must be a string.
Error message
${label}.${key} must be a string. What it means
Thrown by parseJSONObjectField() in web/frontend/src/components/config/form-model.ts:526 while walking the entries of a successfully parsed JSON object. Every value must be a string; the first non-string value (number, boolean, nested object, array, or null) aborts with this message naming the offending key. The function's return type Record<string, string> is why nesting is not supported.
Source
Thrown at web/frontend/src/components/config/form-model.ts:526
return {}
}
let parsed: unknown
try {
parsed = JSON.parse(trimmed)
} catch {
throw new Error(`${label} must be valid JSON.`)
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`${label} must be a JSON object.`)
}
const entries = Object.entries(parsed as Record<string, unknown>)
const result: Record<string, string> = {}
for (const [key, value] of entries) {
if (typeof value !== "string") {
throw new Error(`${label}.${key} must be a string.`)
}
result[key] = value
}
return result
}
View on GitHub (pinned to 49183d7e8d)
Solutions
- Quote all values so they become JSON strings: {"port": "8080", "debug": "true"}
- Flatten nested objects into dot- or underscore-separated keys with string values
- Replace null with an empty string "" if the key must stay present
- Check the exact key named in the message — only that key is invalid
Example fix
// before
{"LOG_LEVEL": "info", "MAX_RETRIES": 3, "opts": {"a": "b"}}
// after
{"LOG_LEVEL": "info", "MAX_RETRIES": "3", "opts.a": "b"} Defensive patterns
Strategy: validation
Validate before calling
function allValuesAreStrings(raw: string): boolean {
const trimmed = raw.trim()
if (trimmed === "") return true
try {
const parsed = JSON.parse(trimmed) as Record<string, unknown>
return Object.values(parsed).every((v) => typeof v === "string")
} catch {
return false
}
} Type guard
function isStringMap(v: unknown): v is Record<string, string> {
return (
!!v &&
typeof v === "object" &&
!Array.isArray(v) &&
Object.values(v as Record<string, unknown>).every((x) => typeof x === "string")
)
} Try / catch
try {
const env = parseJSONObjectField(envText, `MCP server ${name} env`)
} catch (err) {
setFieldError(err instanceof Error ? err.message : "Invalid env JSON") // message names the bad key
} Prevention
- Quote every value: {"PORT": "8080"} not {"PORT": 8080}
- Flat string maps only — never paste nested credential JSON into headers/env fields
- Coerce numbers/booleans to strings when generating the text programmatically
When it happens
Trigger: Putting {"port": 8080} (number), {"debug": true} (boolean), {"nested": {"a": "b"}} (object), or {"x": null} into an MCP server 'headers' or 'env' JSON textarea; same values in a saved baseline server's headersText/envText parsed during save.
Common situations: Users copy provider docs that show numeric env values ("MAX_RETRIES": 3), paste a full nested provider credential JSON instead of a flat string map, or quote keys but not values.
Related errors
- ${label} must be a JSON object.
- MCP discovery requires at least one search method (BM25 or r
- MCP server names must be unique. Duplicates: ${duplicateName
- MCP server ${server.name} requires a URL.
- MCP server ${server.name} requires a valid HTTP(S) URL.
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/92ed764bcbe9f26b.
Report an issue: GitHub.