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

  1. Quote all values so they become JSON strings: {"port": "8080", "debug": "true"}
  2. Flatten nested objects into dot- or underscore-separated keys with string values
  3. Replace null with an empty string "" if the key must stay present
  4. 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

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


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/92ed764bcbe9f26b. Report an issue: GitHub.