ZhuLinsen/daily_stock_analysis · error · TypeError
json_root_not_object
Error message
json_root_not_object
What it means
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.
Source
Thrown at src/analyzer.py:4425
def _load_analysis_json_candidate(self, json_str: str) -> Dict[str, Any]:
"""Parse one already-selected JSON candidate, repairing common LLM JSON drift."""
try:
data = json.loads(json_str)
except json.JSONDecodeError:
stripped = (json_str or "").strip()
try:
_obj, end = json.JSONDecoder().raw_decode(stripped)
except json.JSONDecodeError:
pass
else:
if stripped[end:].strip():
raise
if not (stripped.startswith("{") and stripped.endswith("}")):
raise
repaired = self._fix_json_string(stripped)
data = json.loads(repaired)
if not isinstance(data, dict):
raise TypeError("json_root_not_object")
return data
@staticmethod
def _contains_embedded_json_object(text: str) -> bool:
decoder = json.JSONDecoder()
count = 0
for index, char in enumerate(text):
if char != "{":
continue
try:
_obj, end = decoder.raw_decode(text[index:])
except json.JSONDecodeError:
continue
count += 1
before = text[:index].strip()
after = text[index + end:].strip()
if count > 1 or before or after:
return TrueView on GitHub (pinned to 5159bd72e8)
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.
Example fix
# before
data = analyzer._load_analysis_json_candidate(json_str) # TypeError if root is a list
# after
parsed = json.loads(json_str)
if isinstance(parsed, list):
parsed = {"items": parsed}
if not isinstance(parsed, dict):
raise TypeError("json_root_not_object")
data = parsed Defensive patterns
Strategy: validation
Validate before calling
import json
def root_is_object(json_str: str) -> bool:
try:
return isinstance(json.loads(json_str), dict)
except json.JSONDecodeError:
return False Type guard
def is_analysis_payload(data: object) -> bool:
return isinstance(data, dict) Try / catch
try:
data = analyzer._load_analysis_json_candidate(json_str)
except TypeError as exc:
if str(exc) == 'json_root_not_object':
parsed = json.loads(json_str)
if isinstance(parsed, list):
data = {"items": parsed}
else:
raise
else:
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- invalid_json
- Event alert rules must be a JSON array
- Event alert rules list must contain only objects; invalid en
- Event alert rule must be an object
- schema_validation_failed
AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15).
Data as JSON: /api/errors/1ec9539420c33070.
Report an issue: GitHub.