{"record":{"id":"1ec9539420c33070","repo":"ZhuLinsen/daily_stock_analysis","slug":"json-root-not-object","errorCode":null,"errorMessage":"json_root_not_object","messagePattern":"json_root_not_object","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"src/analyzer.py","lineNumber":4425,"sourceCode":"    def _load_analysis_json_candidate(self, json_str: str) -> Dict[str, Any]:\n        \"\"\"Parse one already-selected JSON candidate, repairing common LLM JSON drift.\"\"\"\n        try:\n            data = json.loads(json_str)\n        except json.JSONDecodeError:\n            stripped = (json_str or \"\").strip()\n            try:\n                _obj, end = json.JSONDecoder().raw_decode(stripped)\n            except json.JSONDecodeError:\n                pass\n            else:\n                if stripped[end:].strip():\n                    raise\n            if not (stripped.startswith(\"{\") and stripped.endswith(\"}\")):\n                raise\n            repaired = self._fix_json_string(stripped)\n            data = json.loads(repaired)\n        if not isinstance(data, dict):\n            raise TypeError(\"json_root_not_object\")\n        return data\n\n    @staticmethod\n    def _contains_embedded_json_object(text: str) -> bool:\n        decoder = json.JSONDecoder()\n        count = 0\n        for index, char in enumerate(text):\n            if char != \"{\":\n                continue\n            try:\n                _obj, end = decoder.raw_decode(text[index:])\n            except json.JSONDecodeError:\n                continue\n            count += 1\n            before = text[:index].strip()\n            after = text[index + end:].strip()\n            if count > 1 or before or after:\n                return True","sourceCodeStart":4407,"sourceCodeEnd":4443,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/analyzer.py#L4407-L4443","documentation":"Raised as TypeError('json_root_not_object') by _load_analysis_json_candidate after a candidate string parses successfully as JSON but the parsed root is not a dict (e.g. a list, a bare number, a string, null, true). The analysis contract requires a top-level JSON object, so any other root type is rejected.","triggerScenarios":"LLM returns a JSON array of analysis items ([{...}, {...}]) instead of an object; LLM returns a quoted string or a bare scalar ('{\"summary\": ...}' inside quotes, 'null', 'true', a number); a repair pass (_fix_json_string) mangles the text so the repaired result decodes to a non-dict.","commonSituations":"Prompt asks for 'a list of findings' and the model obliges with a top-level array; model wraps JSON in quotes thinking it should return a string; schema drift after prompt edits where the expected object shape was not re-communicated.","solutions":["Fix the prompt/contract to require a top-level JSON object with the expected keys, and show a one-shot example object.","If a list is legitimately possible, wrap it at generation time ({\"items\": [...]}) rather than accepting arrays downstream.","Inspect the raw LLM output in debug mode (--debug or the saved raw response) to see which non-dict root the model produced before changing code.","If a repair path produced it, add a regression test with the exact malformed text so _fix_json_string preserves the object root."],"exampleFix":"# before\ndata = analyzer._load_analysis_json_candidate(json_str)  # TypeError if root is a list\n\n# after\nparsed = json.loads(json_str)\nif isinstance(parsed, list):\n    parsed = {\"items\": parsed}\nif not isinstance(parsed, dict):\n    raise TypeError(\"json_root_not_object\")\ndata = parsed","handlingStrategy":"validation","validationCode":"import json\n\ndef root_is_object(json_str: str) -> bool:\n    try:\n        return isinstance(json.loads(json_str), dict)\n    except json.JSONDecodeError:\n        return False","typeGuard":"def is_analysis_payload(data: object) -> bool:\n    return isinstance(data, dict)","tryCatchPattern":"try:\n    data = analyzer._load_analysis_json_candidate(json_str)\nexcept TypeError as exc:\n    if str(exc) == 'json_root_not_object':\n        parsed = json.loads(json_str)\n        if isinstance(parsed, list):\n            data = {\"items\": parsed}\n        else:\n            raise\n    else:\n        raise","preventionTips":["Prompt with a concrete example JSON object, never an array.","Validate the root type immediately after any json.loads of LLM output.","Cover list-rooted outputs in extraction tests so regressions surface early."],"tags":["json","llm-output","type-validation","analyzer"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}