rohitg00/ai-engineering-from-scratch · error · ContractError

expected object

Error message

expected object

What it means

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.

Source

Thrown at certifications/claude/lessons/09-structured-output-and-defensive-parsing/code/main.py:47

    "additionalProperties": False,
    "properties": {
        "category": {"type": "string", "enum": ["billing", "bug", "account", "other"]},
        "priority": {"type": "integer", "minimum": 1, "maximum": 5},
        "summary": {"type": "string", "minLength": 1, "maxLength": 240},
        "needs_human": {"type": "boolean"},
    },
}


def parse_and_validate(raw: str, schema: dict[str, Any]) -> Any:
    """Accept exactly one JSON value, then validate the supported schema subset."""
    try:
        value = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise ContractError([ValidationIssue("$", f"invalid JSON at character {exc.pos}")]) from exc
    issues = validate(value, schema)
    if issues:
        raise ContractError(issues)
    return value


def validate(value: Any, schema: dict[str, Any], path: str = "$") -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    expected = schema.get("type")
    if expected == "object":
        if not isinstance(value, dict):
            return [ValidationIssue(path, "expected object")]
        properties = schema.get("properties", {})
        for name in schema.get("required", []):
            if name not in value:
                issues.append(ValidationIssue(f"{path}.{name}", "required field is missing"))
        if schema.get("additionalProperties") is False:
            for name in value:
                if name not in properties:
                    issues.append(ValidationIssue(f"{path}.{name}", "unexpected field"))
        for name, child in properties.items():

View on GitHub (pinned to 39ea8a1c6d)

Solutions

  1. 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 '{...}'.
  2. If you control generation, instruct 'respond with only a JSON object' and run through BoundedExtractor so the repair loop retries with feedback.
  3. If a non-object is legitimately possible, branch on the json.loads result type before validating, or relax the schema type.
  4. In tests, make fixtures start with '{' and end with '}' and assert with pytest.raises(ContractError).

Example fix

# before
value = parse_and_validate('"billing question"', TRIAGE_SCHEMA)
# ContractError: $: expected object

# after
value = parse_and_validate('{"category": "billing", "priority": 2, "summary": "refund", "needs_human": false}', TRIAGE_SCHEMA)
Defensive patterns

Strategy: try-catch

Validate before calling

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

Type guard

from typing import Any
def is_contract_object(value: Any) -> bool:
    return isinstance(value, dict)

Try / catch

try:
    data = parse_and_validate(raw, TRIAGE_SCHEMA)
except ContractError as exc:
    for issue in exc.issues:
        print(f"{issue.path}: {issue.message}")  # '$: expected object' means root is not a dict
    data = None

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26). Data as JSON: /api/errors/5d89fde9864299d2. Report an issue: GitHub.