justjavac/wechat-miniapp-radar · error · Error

AI response JSON must be an object.

Error message

AI response JSON must be an object.

What it means

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.

Source

Thrown at lib/ai-client.ts:104

  }
  return null;
}

function extractJsonText(text: string) {
  const trimmed = text.trim();
  const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
  if (fenced?.[1]) return fenced[1].trim();

  const firstBrace = trimmed.indexOf("{");
  const lastBrace = trimmed.lastIndexOf("}");
  if (firstBrace >= 0 && lastBrace > firstBrace) return trimmed.slice(firstBrace, lastBrace + 1);
  return trimmed;
}

function parseJsonObject<T>(text: string): T {
  const parsed = JSON.parse(extractJsonText(text)) as unknown;
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
    throw new Error("AI response JSON must be an object.");
  }
  return parsed as T;
}

function parseCompletionPayload(text: string) {
  if (!text.trim()) return null;
  try {
    return JSON.parse(text) as ChatCompletionResponse;
  } catch {
    return null;
  }
}

async function requestChatCompletion<T>({
  config,
  model,
  messages,
  timeoutMs

View on GitHub (pinned to 02a010ecea)

Solutions

  1. Tighten the system prompt to require a single JSON object (never an array) and show the exact top-level shape with a literal example.
  2. 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).
  3. Rely on createAiJsonCompletion()'s built-in recovery: it catches this throw, records it, and retries config.fallbackModel before returning ok:false.
  4. When ok is false, use the app's rule-based fallback for advisor/scoring.

Example fix

// before: a prompt like 'list the frameworks' invites a top-level array

// after: require a single object wrapper
// Respond with ONE JSON object, e.g. { recommendations: [...] }
// The top level MUST be an object, never an array.
Defensive patterns

Strategy: type-guard

Type guard

function isJsonObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

Try / catch

const result = await createAiJsonCompletion<MyType>({ messages });
if (!result.ok || !result.value) {
  // result.error contains 'AI response JSON must be an object.'
  return ruleBasedFallback();
}
return result.value;

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of justjavac/wechat-miniapp-radar@02a010ecea (2026-08-12). Data as JSON: /api/errors/af7cb13f39e65762. Report an issue: GitHub.