{"record":{"id":"af7cb13f39e65762","repo":"justjavac/wechat-miniapp-radar","slug":"ai-response-json-must-be-an-object","errorCode":null,"errorMessage":"AI response JSON must be an object.","messagePattern":"AI response JSON must be an object\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/ai-client.ts","lineNumber":104,"sourceCode":"  }\n  return null;\n}\n\nfunction extractJsonText(text: string) {\n  const trimmed = text.trim();\n  const fenced = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n  if (fenced?.[1]) return fenced[1].trim();\n\n  const firstBrace = trimmed.indexOf(\"{\");\n  const lastBrace = trimmed.lastIndexOf(\"}\");\n  if (firstBrace >= 0 && lastBrace > firstBrace) return trimmed.slice(firstBrace, lastBrace + 1);\n  return trimmed;\n}\n\nfunction parseJsonObject<T>(text: string): T {\n  const parsed = JSON.parse(extractJsonText(text)) as unknown;\n  if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n    throw new Error(\"AI response JSON must be an object.\");\n  }\n  return parsed as T;\n}\n\nfunction parseCompletionPayload(text: string) {\n  if (!text.trim()) return null;\n  try {\n    return JSON.parse(text) as ChatCompletionResponse;\n  } catch {\n    return null;\n  }\n}\n\nasync function requestChatCompletion<T>({\n  config,\n  model,\n  messages,\n  timeoutMs","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/justjavac/wechat-miniapp-radar/blob/02a010ecea0320b7aa975bb62a5dde271ae630c9/lib/ai-client.ts#L86-L122","documentation":"Thrown by parseJsonObject() in lib/ai-client.ts after the model's text is unwrapped (markdown fences stripped, then sliced to the first..last brace) and JSON.parse succeeds but yields a value that is not a plain object, i.e. null, an array, or a primitive. It is the last check enforcing the JSON-completion contract (an object) before the value is cast to T and returned from requestChatCompletion(). Note a genuinely malformed JSON string surfaces as a SyntaxError from JSON.parse on the line above, which is a different failure not handled here.","triggerScenarios":"The LLM returns valid JSON that is an array (e.g. [{...}]) or a bare primitive/string when the prompt asked for one object; extractJsonText() slices to the first/last brace so an array parses successfully and then fails the object check; a model wraps the answer as a quoted JSON string which parses to a string.","commonSituations":"Free/OpenRouter models that ignore response_format json_object and emit arrays; prompts that say 'return a list' while the schema expects an object wrapper; switching to a model whose default serialization differs; temperature/parameter changes that make the model return a quoted scalar.","solutions":["Tighten the system prompt to require a single JSON object (never an array) and show the exact top-level shape with a literal example.","Enable response_format json_object for a supporting model (OPENROUTER_JSON_RESPONSE_FORMAT_MODELS already does this for a curated set; pick a model from the set or extend it).","Rely on createAiJsonCompletion()'s built-in recovery: it catches this throw, records it, and retries config.fallbackModel before returning ok:false.","When ok is false, use the app's rule-based fallback for advisor/scoring."],"exampleFix":"// before: a prompt like 'list the frameworks' invites a top-level array\n\n// after: require a single object wrapper\n// Respond with ONE JSON object, e.g. { recommendations: [...] }\n// The top level MUST be an object, never an array.","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isJsonObject(value: unknown): value is Record<string, unknown> {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}","tryCatchPattern":"const result = await createAiJsonCompletion<MyType>({ messages });\nif (!result.ok || !result.value) {\n  // result.error contains 'AI response JSON must be an object.'\n  return ruleBasedFallback();\n}\nreturn result.value;","preventionTips":["Pin the response shape in the system prompt with a literal example object.","Prefer models already in OPENROUTER_JSON_RESPONSE_FORMAT_MODELS so response_format json_object is sent.","Always branch on createAiJsonCompletion's ok/value instead of assuming the cast to T is valid."],"tags":["ai","json","parsing","validation"],"backgroundTag":null,"analyzedSha":"02a010ecea0320b7aa975bb62a5dde271ae630c9","analyzedAt":"2026-08-12T16:16:33.227Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}