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
- Retry the LLM call once with the same prompt (transient malformed output is common)
- Strengthen the prompt to insist the reply be a single JSON object with resume_data and inferred_skills keys
- Reduce prompt size / input resume length so output isn't truncated by max_tokens=8192
- 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
- Retry malformed LLM responses once before surfacing an error
- Keep prompts within token budgets to avoid truncated JSON
- Pin/monitor the model and validate complete_json output shape in CI tests
- Use structured output / JSON mode when the provider supports it
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
- JSON extraction exceeded max recursion depth: {_depth}
- Content too large for JSON extraction: {len(content)} bytes
- No JSON found in response: {original[:200]}
- Failed to test LLM connection (status ${res.status}).
- Resume preview data is invalid.
AI-assisted analysis of srbhr/Resume-Matcher@116f9cc3b0 (2026-08-28).
Data as JSON: /api/errors/29f6717d428f47ab.
Report an issue: GitHub.