{"record":{"id":"99d60bc05f095cb1","repo":"sipeed/picoclaw","slug":"label-must-be-a-json-object","errorCode":null,"errorMessage":"${label} must be a JSON object.","messagePattern":"(.+?) must be a JSON object\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"web/frontend/src/components/config/form-model.ts","lineNumber":519,"sourceCode":"\nexport function parseJSONObjectField(\n  rawValue: string,\n  label: string,\n): Record<string, string> {\n  const trimmed = rawValue.trim()\n  if (trimmed === \"\") {\n    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":501,"sourceCodeEnd":532,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/frontend/src/components/config/form-model.ts#L501-L532","documentation":"Thrown by parseJSONObjectField() in web/frontend/src/components/config/form-model.ts:519 when a text field meant to hold a JSON object (e.g. MCP server headers/env) parses as valid JSON but the top-level value is not an object. The function is strict: after JSON.parse succeeds it rejects arrays, null, strings, numbers, and booleans because the return type is Record<string, string>. The label prefixes the field so the user knows which textarea failed.","triggerScenarios":"Entering `[\"a\",\"b\"]`, `\"x\"`, `123`, `true`, or `null` into an MCP server 'headers' or 'env' JSON textarea in the config page and clicking Save; also triggered when parsing a saved baseline server's headersText via parseJSONObjectField(baselineServer.headersText, `Saved MCP server ${server.name} headers`) during save (config-page.tsx:489).","commonSituations":"Users paste a curl-style `-H 'Authorization: Bearer ...'` line instead of a JSON object; users wrap the object in quotes making it a JSON string; users paste a JSON array of header pairs; a previously saved config contains `[]` for headers.","solutions":["Change the field content to a flat JSON object of string values, e.g. {\"Authorization\": \"Bearer tok\", \"X-Org\": \"abc\"}","Remove surrounding quotes or backticks around the object — a quoted object is parsed as a string, not an object","If the error names a 'Saved MCP server ... headers' field, the stored config itself is bad: open the raw config page and fix that server's headers/env value there","Leave the field empty — an empty string short-circuits to {} and is always accepted"],"exampleFix":"// before (field content)\n[\"Authorization: Bearer tok\"]\n\n// after\n{\"Authorization\": \"Bearer tok\"}","handlingStrategy":"validation","validationCode":"function isJSONObjectText(raw: string): boolean {\n  const trimmed = raw.trim()\n  if (trimmed === \"\") return true // empty is allowed\n  try {\n    const parsed = JSON.parse(trimmed)\n    return !!parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n  } catch {\n    return false\n  }\n}\n\n// before parseJSONObjectField(text, label):\nif (!isJSONObjectText(text)) {\n  showFieldError(`${label} must be a JSON object (e.g. {\"Key\": \"value\"}).`)\n  return\n}","typeGuard":"function isRecord(v: unknown): v is Record<string, unknown> {\n  return !!v && typeof v === \"object\" && !Array.isArray(v)\n}","tryCatchPattern":"try {\n  const headers = parseJSONObjectField(text, \"MCP server x headers\")\n} catch (err) {\n  // err.message already carries the label; surface it next to the field\n  setFieldError(err instanceof Error ? err.message : \"Invalid JSON field\")\n}","preventionTips":["Validate JSON textareas on blur, not only at save time, so users learn the expected shape immediately","Show a placeholder example like {\"Authorization\": \"Bearer token\"} in headers/env fields","Lint saved configs once at load so corrupt baseline headersText fails with a named server instead of mid-save"],"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"}