{"record":{"id":"5d89fde9864299d2","repo":"rohitg00/ai-engineering-from-scratch","slug":"expected-object","errorCode":null,"errorMessage":"expected object","messagePattern":"expected object","errorType":"validation","errorClass":"ContractError","httpStatus":null,"severity":"error","filePath":"certifications/claude/lessons/09-structured-output-and-defensive-parsing/code/main.py","lineNumber":47,"sourceCode":"    \"additionalProperties\": False,\n    \"properties\": {\n        \"category\": {\"type\": \"string\", \"enum\": [\"billing\", \"bug\", \"account\", \"other\"]},\n        \"priority\": {\"type\": \"integer\", \"minimum\": 1, \"maximum\": 5},\n        \"summary\": {\"type\": \"string\", \"minLength\": 1, \"maxLength\": 240},\n        \"needs_human\": {\"type\": \"boolean\"},\n    },\n}\n\n\ndef parse_and_validate(raw: str, schema: dict[str, Any]) -> Any:\n    \"\"\"Accept exactly one JSON value, then validate the supported schema subset.\"\"\"\n    try:\n        value = json.loads(raw)\n    except json.JSONDecodeError as exc:\n        raise ContractError([ValidationIssue(\"$\", f\"invalid JSON at character {exc.pos}\")]) from exc\n    issues = validate(value, schema)\n    if issues:\n        raise ContractError(issues)\n    return value\n\n\ndef validate(value: Any, schema: dict[str, Any], path: str = \"$\") -> list[ValidationIssue]:\n    issues: list[ValidationIssue] = []\n    expected = schema.get(\"type\")\n    if expected == \"object\":\n        if not isinstance(value, dict):\n            return [ValidationIssue(path, \"expected object\")]\n        properties = schema.get(\"properties\", {})\n        for name in schema.get(\"required\", []):\n            if name not in value:\n                issues.append(ValidationIssue(f\"{path}.{name}\", \"required field is missing\"))\n        if schema.get(\"additionalProperties\") is False:\n            for name in value:\n                if name not in properties:\n                    issues.append(ValidationIssue(f\"{path}.{name}\", \"unexpected field\"))\n        for name, child in properties.items():","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/rohitg00/ai-engineering-from-scratch/blob/39ea8a1c6d0b61f071226eff7ede4d4105fed820/certifications/claude/lessons/09-structured-output-and-defensive-parsing/code/main.py#L29-L65","documentation":"ContractError '$: expected object' is raised by parse_and_validate when the parsed JSON root is not a dict but the schema declares type 'object'. The lesson's validate() checks isinstance(value, dict) at the root and returns the single issue; parse_and_validate wraps collected issues into one ContractError (a ValueError subclass carrying .issues). It enforces that model output strictly matches the JSON schema contract instead of silently coercing.","triggerScenarios":"Calling parse_and_validate(raw, schema) where schema has \"type\": \"object\" and raw is valid JSON but a non-object: '\"just a string\"', '[1,2,3]', '42', 'true', 'null'. Also hit in BoundedExtractor.extract when the generate stub returns a bare JSON scalar, and by tests like test_valid_object_is_returned feeding scalar payloads.","commonSituations":"LLM returns a bare scalar or array instead of an object (e.g. answers with just a summary string); markdown-fenced output parsed to something unexpected; prompt drift where the model stops emitting the object; hand-written test fixtures that omit the outer braces.","solutions":["Inspect exc.issues[0].path (it will be '$') to confirm the root itself failed, then fix the payload or prompt so the output is a JSON object literal '{...}'.","If you control generation, instruct 'respond with only a JSON object' and run through BoundedExtractor so the repair loop retries with feedback.","If a non-object is legitimately possible, branch on the json.loads result type before validating, or relax the schema type.","In tests, make fixtures start with '{' and end with '}' and assert with pytest.raises(ContractError)."],"exampleFix":"# before\nvalue = parse_and_validate('\"billing question\"', TRIAGE_SCHEMA)\n# ContractError: $: expected object\n\n# after\nvalue = parse_and_validate('{\"category\": \"billing\", \"priority\": 2, \"summary\": \"refund\", \"needs_human\": false}', TRIAGE_SCHEMA)","handlingStrategy":"try-catch","validationCode":"import json\ndef is_json_object(raw: str) -> bool:\n    try:\n        return isinstance(json.loads(raw), dict)\n    except json.JSONDecodeError:\n        return False","typeGuard":"from typing import Any\ndef is_contract_object(value: Any) -> bool:\n    return isinstance(value, dict)","tryCatchPattern":"try:\n    data = parse_and_validate(raw, TRIAGE_SCHEMA)\nexcept ContractError as exc:\n    for issue in exc.issues:\n        print(f\"{issue.path}: {issue.message}\")  # '$: expected object' means root is not a dict\n    data = None","preventionTips":["Prompt the model to 'reply with only a JSON object' and show an example with braces.","Pre-check json.loads(raw) is a dict before full validation.","Echo the '$' issue back to the model as repair feedback in retry loops."],"tags":["json","validation","schema","structured-output"],"backgroundTag":"json-schema-type-mismatch","analyzedSha":"39ea8a1c6d0b61f071226eff7ede4d4105fed820","analyzedAt":"2026-08-26T03:13:46.626Z","schemaVersion":2},"datasetVersion":"2026-08-26T07:17:17.940Z"}