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

  1. Re-run describe(); LLM output is nondeterministic and a retry often yields parseable JSON (the caller already retries up to 3 attempts in describe).
  2. 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.
  3. Enable/verify response_format JSON mode or lower temperature on the vision model call so output is constrained to JSON.
  4. Increase max_tokens so the object is not truncated mid-way, and strip markdown code fences before parsing.
  5. 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

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


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 = False

View on GitHub (pinned to 8321021c54)