Structured Output and Validation Loops
Domain 4.3 and 4.4: guaranteeing schema-compliant output with tool use and JSON schemas, the tool_choice modes that force it, why strict schemas fix syntax but not semantics, and the retry-with-error-feedback loop — plus knowing when a retry can't possibly help.
The second half of Domain 4 is getting reliable, structured output — data your downstream systems can consume without a fragile parser — and then validating it. This is the Structured Data Extraction scenario’s core: extract fields, guarantee the shape, catch the errors the shape can’t. Tasks 4.3 (structured output) and 4.4 (validation loops). The tool_use + tool_choice machinery is verified against anthropic 0.120.0; live extraction quality carries a ⚠️ for the key-based pass.
Tool use is how you guarantee the shape
The most reliable way to get schema-compliant structured output from Claude is tool use with a JSON schema. You define an “extraction tool” whose input schema is the shape you want, force the model to call it, and read the structured data out of the resulting tool_use block. Because the model is filling a tool’s typed arguments rather than free-writing JSON, this eliminates JSON syntax errors — no missing commas, no unquoted keys, no truncated braces.
extract_invoice = {
"name": "extract_invoice",
"description": "Extract structured fields from an invoice.",
"input_schema": {
"type": "object",
"properties": {
"invoice_number": {"type": "string"},
"total": {"type": "number"},
"line_items": {"type": "array", "items": {...}},
},
"required": ["invoice_number", "total"],
},
}
resp = client.messages.create(
model="claude-haiku-4-5", max_tokens=1024,
tools=[extract_invoice],
tool_choice={"type": "tool", "name": "extract_invoice"}, # force this exact tool
messages=[{"role": "user", "content": document}],
)
data = next(b.input for b in resp.content if b.type == "tool_use") # the structured result
The tool_choice control (verified) is what turns “structured output is likely” into “structured output is guaranteed”:
- Forced —
{"type": "tool", "name": "extract_invoice"}— the model must call that exact tool. Use it when you know the schema you want. "any"— the model must call a tool but chooses which. Use it when several extraction schemas exist and the document type is unknown — the model picks the right extractor."auto"— the model may return text instead. Wrong for guaranteed extraction, because it leaves the door open to a prose reply your parser chokes on.
Schemas fix syntax, not semantics
Here is the exam trap in this area, and it’s a good one: a strict JSON schema eliminates syntax errors but does nothing about semantic ones. The output will be well-formed and type-correct — and still wrong. Line items that don’t sum to the stated total, a value placed in the wrong field, a date that parses but is nonsensical: all of these satisfy the schema perfectly. Schema validation guarantees the shape, never the correctness of the content — the same distinction that structured output in general can’t escape. If an exam item claims “we use a strict schema, so the extraction is validated,” that’s the trap; the schema validated the shape, and the numbers can still be nonsense.
Two schema-design skills mitigate this, both about not forcing the model to lie:
- Make fields optional/nullable when the source may not contain them. A
requiredfield the document doesn’t have forces the model to fabricate a value to satisfy the schema. Marking it optional lets the model return null honestly instead of inventing data — a direct hallucination reducer. - Use enums with an escape hatch. For a categorization field, add an
"unclear"value for ambiguous cases and an"other"+ free-textdetailfield for cases outside your categories. Without them, the model jams a genuinely-other case into the nearest wrong bucket. And include format-normalization rules in the prompt alongside the schema, so inconsistent source formatting (dates, currencies) comes out uniform.
Validation and retry loops
Because schemas don’t catch semantic errors, you validate the output yourself and retry when it’s wrong. The core technique is retry-with-error-feedback: when validation fails, send a follow-up that includes the original document, the failed extraction, and the specific validation errors, so the model can self-correct against precise feedback rather than a vague “try again.”
Your extraction had errors. Fix them and re-extract.
- line_items sum to 240.00 but total is 260.00 (discrepancy of 20.00)
- ship_date "2026-13-02" is not a valid date
[original document follows]
The design pattern that makes semantic validation possible is having the model extract the checkable pieces: pull calculated_total (sum of line items) alongside stated_total, so your validator can compare them and flag a discrepancy; add a conflict_detected boolean when the source data is internally inconsistent. You’re designing the schema so that correctness becomes checkable, not just shape-conformant. (⚠️ live self-correction behavior pending the key pass; the schema and tool_choice mechanics are verified.)
When a retry can’t help
The most important judgment in 4.4, because it separates a well-designed loop from one that burns turns forever: retries only help when the error is fixable from what the model has. Format errors and structural output mistakes are fixable — the information is present, the model just rendered it wrong, and error-feedback corrects it. But if the required information is simply absent from the source document — the invoice never stated a PO number — no amount of retrying will conjure it, and each retry wastes a call producing the same null or the same fabrication.
So a good validation loop distinguishes the two: retry on format/structural errors, and on “information missing” stop — return the field as null with a note, or route to a human, rather than looping. An exam item describing an extraction pipeline that “retries up to 5 times on any validation failure” is describing a loop that will burn all five retries on a field the document never contained. The right design retries what’s fixable and gives up early on what isn’t.
Final thoughts
Guaranteed structured output is tool use with a JSON schema, forced (or "any") via tool_choice so the model can’t return prose — which eliminates syntax errors but not semantic ones, so a strict schema is never a correctness guarantee. Schema design reduces fabrication (optional/nullable fields, "unclear"/"other" enums) and validation loops catch what schemas can’t, using retry-with-specific-error-feedback and checkable fields like calculated_total versus stated_total. The judgment that separates good from bad: retry format and structural errors, but recognize when the information is absent and stop instead of looping. Next: doing this at scale, and reviewing it well.
Next: batch processing and multi-pass review — the Message Batches API’s trade-offs, and why an independent reviewer beats a self-review.
Comments