srbhr/Resume-Matcher · error · ValueError

Resume wizard LLM response must be a JSON object.

Error message

Resume wizard LLM response must be a JSON object.

What it means

run_ai_turn calls complete_json expecting the LLM to return a parsed JSON object. If the model's response parses to a non-dict (string, list, number, or None), it raises this ValueError because the subsequent result.get("resume_data") contract requires a mapping.

Source

Thrown at apps/backend/app/services/resume_wizard.py:362

    section = state.current_question.section
    resume_json = json.dumps(state.resume_data.model_dump(mode="json"), ensure_ascii=False)
    prompt_answer = (
        "(The user skipped this question. Do NOT modify resume_data. "
        "Ask the next most useful question for a different section.)"
        if skip
        # Strip prompt-injection patterns AND redact credential-like tokens
        # (sk-…/AIza…/Bearer …) before the answer reaches the LLM.
        else _scrub_secrets(_sanitize_user_input(answer_text))
    )
    prompt = RESUME_WIZARD_TURN_PROMPT.format(
        output_language=get_language_name(get_content_language()),
        current_section=section,
        resume_json=resume_json,
        answer_text=prompt_answer,
    )
    result = await complete_json(prompt, max_tokens=8192, schema_type="resume")
    if not isinstance(result, dict):
        raise ValueError("Resume wizard LLM response must be a JSON object.")

    raw_resume = result.get("resume_data")
    inferred = _string_list(result.get("inferred_skills"))

    if skip or not isinstance(raw_resume, dict):
        data = state.resume_data.model_copy(deep=True)
    else:
        updated = ResumeData.model_validate(normalize_wizard_resume_data(raw_resume))
        data = _merge_section(
            existing=state.resume_data,
            updated=updated,
            raw_updated=raw_resume,
            section=section,
            inferred_skills=inferred,
        )

    if section == "intro" and not data.personalInfo.name.strip():
        fallback = extract_intro_name(answer_text)

View on GitHub (pinned to 116f9cc3b0)

Solutions

  1. Retry the LLM call once with the same prompt (transient malformed output is common)
  2. Strengthen the prompt to insist the reply be a single JSON object with resume_data and inferred_skills keys
  3. Reduce prompt size / input resume length so output isn't truncated by max_tokens=8192
  4. Wrap the call in try/except ValueError and return a 502/friendly error so the wizard can recover

Example fix

// before
result = await complete_json(prompt, max_tokens=8192, schema_type="resume")
if not isinstance(result, dict):
    raise ValueError("Resume wizard LLM response must be a JSON object.")
// after
result = await complete_json(prompt, max_tokens=8192, schema_type="resume")
if not isinstance(result, dict):
    logger.warning("wizard LLM returned %s, retrying", type(result).__name__)
    result = await complete_json(prompt, max_tokens=8192, schema_type="resume")
if not isinstance(result, dict):
    raise ValueError("Resume wizard LLM response must be a JSON object.")
Defensive patterns

Strategy: type-guard

Validate before calling

import json
def llm_reply_is_object(raw: str) -> bool:
    try:
        return isinstance(json.loads(raw), dict)
    except Exception:
        return False

Type guard

def is_llm_result_dict(v: object) -> TypeGuard[dict]:
    return isinstance(v, dict)

Try / catch

try:
    result = await run_ai_turn(state, action, answer)
except ValueError as e:
    if "must be a JSON object" in str(e):
        result = await run_ai_turn(state, action, answer)  # one retry
    else:
        raise

Prevention

When it happens

Trigger: LLM returns a bare JSON array/string, returns prose instead of JSON that still parses oddly, or complete_json returns a non-dict fallback; occurs in resume_wizard_turn and its test paths.

Common situations: Model degradation or prompt too long causing truncated/garbled output; temperature/sampling changes; schema_type="resume" contract drift in complete_json; max_tokens=8192 truncation cutting the object.

Related errors


AI-assisted analysis of srbhr/Resume-Matcher@116f9cc3b0 (2026-08-28). Data as JSON: /api/errors/29f6717d428f47ab. Report an issue: GitHub.