{"record":{"id":"92ed764bcbe9f26b","repo":"sipeed/picoclaw","slug":"label-key-must-be-a-string","errorCode":null,"errorMessage":"${label}.${key} must be a string.","messagePattern":"(.+?)\\.(.+?) must be a string\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"web/frontend/src/components/config/form-model.ts","lineNumber":526,"sourceCode":"    return {}\n  }\n\n  let parsed: unknown\n  try {\n    parsed = JSON.parse(trimmed)\n  } catch {\n    throw new Error(`${label} must be valid JSON.`)\n  }\n\n  if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n    throw new Error(`${label} must be a JSON object.`)\n  }\n\n  const entries = Object.entries(parsed as Record<string, unknown>)\n  const result: Record<string, string> = {}\n  for (const [key, value] of entries) {\n    if (typeof value !== \"string\") {\n      throw new Error(`${label}.${key} must be a string.`)\n    }\n    result[key] = value\n  }\n  return result\n}\n","sourceCodeStart":508,"sourceCodeEnd":532,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/frontend/src/components/config/form-model.ts#L508-L532","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n{\"LOG_LEVEL\": \"info\", \"MAX_RETRIES\": 3, \"opts\": {\"a\": \"b\"}}\n\n// after\n{\"LOG_LEVEL\": \"info\", \"MAX_RETRIES\": \"3\", \"opts.a\": \"b\"}","handlingStrategy":"validation","validationCode":"function allValuesAreStrings(raw: string): boolean {\n  const trimmed = raw.trim()\n  if (trimmed === \"\") return true\n  try {\n    const parsed = JSON.parse(trimmed) as Record<string, unknown>\n    return Object.values(parsed).every((v) => typeof v === \"string\")\n  } catch {\n    return false\n  }\n}","typeGuard":"function isStringMap(v: unknown): v is Record<string, string> {\n  return (\n    !!v &&\n    typeof v === \"object\" &&\n    !Array.isArray(v) &&\n    Object.values(v as Record<string, unknown>).every((x) => typeof x === \"string\")\n  )\n}","tryCatchPattern":"try {\n  const env = parseJSONObjectField(envText, `MCP server ${name} env`)\n} catch (err) {\n  setFieldError(err instanceof Error ? err.message : \"Invalid env JSON\") // message names the bad key\n}","preventionTips":["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"],"tags":["json","validation","frontend","mcp","config"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}