sipeed/picoclaw · warning

${label} must be a JSON object.

Error message

${label} must be a JSON object.

What it means

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.

Source

Thrown at web/frontend/src/components/config/form-model.ts:519

export function parseJSONObjectField(
  rawValue: string,
  label: string,
): Record<string, string> {
  const trimmed = rawValue.trim()
  if (trimmed === "") {
    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. Change the field content to a flat JSON object of string values, e.g. {"Authorization": "Bearer tok", "X-Org": "abc"}
  2. Remove surrounding quotes or backticks around the object — a quoted object is parsed as a string, not an object
  3. 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
  4. Leave the field empty — an empty string short-circuits to {} and is always accepted

Example fix

// before (field content)
["Authorization: Bearer tok"]

// after
{"Authorization": "Bearer tok"}
Defensive patterns

Strategy: validation

Validate before calling

function isJSONObjectText(raw: string): boolean {
  const trimmed = raw.trim()
  if (trimmed === "") return true // empty is allowed
  try {
    const parsed = JSON.parse(trimmed)
    return !!parsed && typeof parsed === "object" && !Array.isArray(parsed)
  } catch {
    return false
  }
}

// before parseJSONObjectField(text, label):
if (!isJSONObjectText(text)) {
  showFieldError(`${label} must be a JSON object (e.g. {"Key": "value"}).`)
  return
}

Type guard

function isRecord(v: unknown): v is Record<string, unknown> {
  return !!v && typeof v === "object" && !Array.isArray(v)
}

Try / catch

try {
  const headers = parseJSONObjectField(text, "MCP server x headers")
} catch (err) {
  // err.message already carries the label; surface it next to the field
  setFieldError(err instanceof Error ? err.message : "Invalid JSON field")
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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