{"record":{"id":"1ce85da8f2abe35e","repo":"666ghj/MiroFish","slug":"llm-json-response-must-be-a-top-level-json-object","errorCode":null,"errorMessage":"LLM JSON response must be a top-level JSON object","messagePattern":"LLM JSON response must be a top-level JSON object","errorType":"exception","errorClass":"LLMResponseError","httpStatus":502,"severity":"error","filePath":"backend/app/utils/llm_client.py","lineNumber":285,"sourceCode":"                value, end = json.JSONDecoder().raw_decode(content)\n            except json.JSONDecodeError:\n                raise LLMResponseError(\n                    \"LLM returned invalid JSON \"\n                    f\"(line {strict_error.lineno}, column {strict_error.colno})\",\n                    finish_reason=finish_reason,\n                ) from strict_error\n\n            trailing = content[end:].strip()\n            if trailing:\n                if _contains_additional_json_container(trailing):\n                    raise LLMResponseError(\n                        \"LLM returned multiple JSON values\",\n                        finish_reason=finish_reason,\n                    )\n                logger.warning(\"Ignoring text after a complete LLM JSON object\")\n\n        if not isinstance(value, dict):\n            raise LLMResponseError(\n                \"LLM JSON response must be a top-level JSON object\",\n                finish_reason=finish_reason,\n            )\n\n        return value\n","sourceCodeStart":267,"sourceCodeEnd":291,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/backend/app/utils/llm_client.py#L267-L291","documentation":"LLMResponseError raised after successful parsing when the decoded value is not a dict — e.g. the model returned a JSON array, string, number, or null. The client's contract is a top-level JSON object (it returns Dict[str, Any]), so non-object roots are rejected even though they are valid JSON.","triggerScenarios":"Prompt/schema leads the model to answer with '[...]' (a list of items) or a bare scalar; json.loads succeeds, isinstance(value, dict) fails.","commonSituations":"Prompts that say 'return a list of entities' (model naturally emits a top-level array), or few-shot examples showing array roots.","solutions":["Adjust the prompt/schema to require an object root: {'items': [...]} instead of a bare array","Update few-shot examples to show object roots only","If a list output is actually desired, wrap it client-side: parse {'items': [...]} and use value['items']","Use response_format json_object plus an explicit schema description of the object shape"],"exampleFix":"# before\nprompt = \"Return a JSON list of sub-questions.\"\n\n# after\nprompt = \"Return a JSON object like {\\\"sub_questions\\\": [\\\"...\\\", \\\"...\\\"]}.\"","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"def is_json_object_response(content: str) -> bool:\n    try:\n        return isinstance(json.loads(content), dict)\n    except json.JSONDecodeError:\n        return False","tryCatchPattern":"try:\n    value = LLMClient._parse_json_response(resp)\nexcept LLMResponseError as e:\n    if \"top-level JSON object\" in str(e):\n        value = client.generate_json(object_root_prompt())\n    else:\n        raise","preventionTips":["Specify the exact object shape in the prompt, including array-valued keys","Never prompt with 'return a list' when the client contract is a dict","Validate model-facing schemas against the parser's dict requirement"],"tags":["llm","json","schema","contract"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}