affaan-m/ECC · error · ValueError
no JSON object found in response
Error message
no JSON object found in response: {text[:200]!r} What it means
extract_json scans the vision model's raw response text for a balanced JSON object and parses it with json.loads. It raises ValueError when no substring of the response parses to a dict, meaning the model returned prose, a truncated answer, a JSON array, or malformed JSON instead of an object. This is the guard ensuring downstream code only ever handles dict-shaped model output.
Solutions
- Re-run describe(); LLM output is nondeterministic and a retry often yields parseable JSON (the caller already retries up to 3 attempts in describe).
- Inspect the response snippet in the error message (first 200 chars) to see what the model actually returned and adjust the prompt to demand a bare JSON object.
- Enable/verify response_format JSON mode or lower temperature on the vision model call so output is constrained to JSON.
- Increase max_tokens so the object is not truncated mid-way, and strip markdown code fences before parsing.
- If the model consistently returns arrays, adapt the prompt schema or post-process arrays into objects before calling extract_json.
Example fix
// before
obj = extract_json(raw_text)
// after
import json, re
def extract_json_safe(raw_text: str) -> dict:
stripped = re.sub(r"^```(?:json)?|```$", "", raw_text.strip(), flags=re.M).strip()
try:
obj = json.loads(stripped)
except json.JSONDecodeError as exc:
raise ValueError(f"model returned non-JSON output: {stripped[:200]!r}") from exc
if not isinstance(obj, dict):
raise ValueError(f"expected a JSON object, got {type(obj).__name__}")
return obj Defensive patterns
Strategy: try-catch
Validate before calling
def looks_like_json_object(text: str) -> bool:
t = text.strip()
return t.startswith("{") and t.endswith("}") Type guard
def is_dict(obj: object) -> TypeGuard[dict]:
return isinstance(obj, dict) Try / catch
try:
obj = extract_json(raw)
except ValueError as exc:
log.warning("non-JSON model output: %s", exc)
obj = retry_with_stricter_prompt(raw) Prevention
- Use the model's JSON/response_format mode when available
- Strip markdown code fences from LLM output before parsing
- Set max_tokens high enough that the JSON object is never truncated
- Log the raw response on parse failure for quick diagnosis
- Prompt explicitly: 'respond with a single bare JSON object and nothing else'
When it happens
Trigger: extract_json is called by describe() with the LLM response text; it raises when the text contains no '{', contains unbalanced braces, contains a JSON array/scalar only, or all candidate spans fail json.loads (e.g. trailing commas, single quotes, markdown fences wrapping malformed JSON).
Common situations: Model returns a conversational refusal or explanation instead of JSON; model wraps JSON in ```json fences with stray text; model returns a JSON array instead of an object; response is truncated by max_tokens leaving unbalanced braces; model uses non-strict JSON (trailing commas, comments).
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
- Cannot read JSON object
- gh returned invalid JSON
- gh returned invalid JSON
- Malformed coordination JSON in body
- Memory frontmatter field in
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/6c94b32bdcad4efa.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-distillation/scripts/distill.py:112
for chunk in candidates:
chunk = chunk.strip()
try:
obj = json.loads(chunk)
if isinstance(obj, dict):
return obj
except json.JSONDecodeError:
pass
span = _balanced_object(chunk)
if span:
try:
obj = json.loads(span)
if isinstance(obj, dict):
return obj
except json.JSONDecodeError:
continue
raise ValueError(f"no JSON object found in response: {text[:200]!r}")
def _balanced_object(text: str) -> str | None:
start = text.find("{")
if start < 0:
return None
depth = 0
in_str = False
esc = False
for i in range(start, len(text)):
ch = text[i]
if in_str:
if esc:
esc = False
elif ch == "\\":
esc = True
elif ch == '"':
in_str = FalseView on GitHub (pinned to 8321021c54)