Output
Free-form text is for humans to read. Code consumes data with shapes, types, and labels. This module turns Claude's output into something your code can actually trust — reliably, every time, with self-correcting recovery when things go sideways.
The 5 concepts of Structured Output
Tap any concept to jump to its 5-card cluster.
- 1Why Agents Need StructureThe contract between Claude and your code
- 2Tool Use as Structured OutputForcing Claude to fill in a JSON form
- 3Validation & Schema CheckingDefense-in-depth with Pydantic / Zod
- 4Error RecoveryRetry with the actual error in the prompt
- 5Multi-Modal InputVision and PDFs for messy real documents
Code reads data, not paragraphs
An agent is not a chatbot. The output of one step becomes the input to the next: a database write, an API call, a UI render, a routing decision. All of those expect labeled fields with predictable types — not a friendly sentence written for a human reader.
Structured output is the contract between Claude and the rest of your system. Without it, every downstream step becomes fragile string parsing that breaks on edge cases at 2 AM. With it, you go from "it usually works" to "it always works or it fails loudly with a specific error."
The rambling coworker
BEFORE: You ask a coworker for customer data. They reply: "Oh yeah, John works over at Acme, I think his email is something like john@acme.com, and he might be a PM?" — rambling, hedged, conversational.
PAIN: Now imagine your code has to parse that hundreds of times per minute. Regex breaks on edge cases. Sentence structures vary wildly. One missed field crashes the entire pipeline at 2 AM, and the on-call engineer spends two hours hunting the cause.
MAPPING: Structured data is the same coworker handing you a filled-in spreadsheet instead of a paragraph. Every field has a label. Every value has a type. Your code grabs exactly what it needs with zero guesswork.
From text to action
- Claude generatesThe model produces output. Free-form by default; structured if you constrain it.
- Your code consumesA parser turns the output into native objects: dicts, classes, records.
- Downstream actsThose objects feed a DB insert, an API call, a UI render, or a branch decision.
- One bad shape breaks everythingIf field names drift or types mismatch, every consumer downstream fails — often silently.
Do you need structured output?
# Question: should this Claude call return free text or structured JSON? IF output is shown directly to a human reader: free text is fine # chat reply, summary, draft email ELIF code does IF/SWITCH on the answer: REQUIRE structured output # routing, classification ELIF output feeds a DB / API / queue: REQUIRE structured output # every field typed ELIF volume > 100 calls/day: REQUIRE structured output # even 2% parse fail = 2 broken/day ELSE: START with free text, upgrade when it breaks
Rule of thumb: if your code does an if on Claude's answer, you need structured output. Period.
Misconceptions
Structured output is the contract between Claude and the rest of your code. Without it, every downstream step becomes regex-and-prayer.
Pick structure whenever code — not a human — reads the output. The cost is one extra schema definition; the payoff is a pipeline that fails loudly instead of silently.
Tool use IS structured output
Claude's tool_use feature was originally designed so the model could call functions. The same mechanism is also Claude's most reliable structured-output channel. You define a tool with a JSON Schema. You force Claude to use it with tool_choice. Claude returns a tool_use block whose input field is guaranteed to match the schema's shape.
The trick: you don't have to actually execute the tool. Define a "tool" purely as a typed form — Claude fills it in, you read the values out of input, and that's your structured response. Benchmarks: prompt-only JSON extraction fails 5–15% of the time; tool-use extraction fails under 0.5%.
The restaurant order form
BEFORE: Without tool use, getting structured output meant shouting your order across a noisy room. "Please return JSON with name, email, phone — and no markdown around it!" — and hoping the model heard you correctly.
PAIN: The model would return prose with JSON embedded, or wrap it in ```json fences, or add "Here's the data:" before it. Five different malformations meant five different regex patches, all fragile.
MAPPING: Tool use is the printed order form at a restaurant. Pre-printed boxes for dish, quantity, special instructions. Claude is the diner who can only fill in the blanks — physically cannot return a free-form story when forced to use the form. The schema guarantees the structure.
The four-step pattern
- Define a toolGive it a name, a description, and an input_schema — the exact JSON Schema describing the shape you want back.
- Force its useSet
tool_choice: { type: "tool", name: "your_tool" }. Claude can no longer respond with plain text. - Read the blockLoop through the response content blocks. Find the one with
type == "tool_use". Itsinputfield is your typed data. - Skip executionFor pure structured output you never call any function. The "tool" was just a schema. Claude's
stop_reasonwill betool_useand you simply read the input.
Pro tip: auto-generate the input_schema from a Pydantic model with Model.model_json_schema() so the schema and your validator never drift apart.
Extract a contact
# A "tool" you never actually execute — # the schema IS the structured-output contract. DEFINE tool "extract_contact": description: "Pull contact fields from text" input_schema: name : string # required email : string # required phone : string # optional company : string # optional role : string # optional SEND to Claude: tools : [extract_contact] tool_choice : { type: "tool", name: "extract_contact" } message : "Extract contact: " + text FOR block IN response.content: IF block.type == "tool_use": contact = VALIDATE(block.input) # Pydantic / Zod RETURN contact RAISE "no tool_use block returned"
Misconceptions
tool_use guarantees the values are correct."Tool use is the structured-output channel. Define a tool, force it with tool_choice, read the typed input, skip execution. Schema-valid output rate jumps from ~90% to 99%+.
This pattern is the foundation. Every other technique in this module — validation, retry, multi-modal — layers on top of it.
Defense in depth, three layers
Reliable structured output stacks three independent safety layers. Each catches a different class of failure. If one slips, the next catches it. Together they push schema-valid rates near 100%.
Layer 1 — Format constraints: tool use (or stop sequences for prompt-only flows) makes Claude emit the right shape from the start. Layer 2 — Syntactic check: json.loads() / JSON.parse() confirms the bytes are valid JSON. Layer 3 — Semantic check: Pydantic / Zod confirms types, required fields, ranges, and enums are correct for your domain.
Director, editor, inspector
BEFORE: Imagine a film shoot with no quality controls. The actor improvises, the camera keeps rolling, and the raw footage goes straight to release. Eventually a take with someone's phone ringing in the background ships to theaters.
PAIN: In production code that means a malformed JSON response slipping into a database, an enum value the API rejects, a price field that's a string when it should be a number. The error doesn't surface until thousands of records are corrupted.
MAPPING: Tool use is the director shaping the take in real time. json.loads() is the editor cutting at the closing brace so no audio bleed remains. Pydantic / Zod is the quality inspector watching every frame before release. Three roles, three catches.
The validation pipeline
- Constrain the formatTool use returns a typed block. (For prompt-only flows, set
stop_sequences: ["}"]so Claude halts at the closing brace and never appends prose.) - Parse the JSONRun
json.loads()/JSON.parse(). This catches syntax errors — missing commas, mismatched braces, escape glitches. - Validate the schemaPass the parsed dict through a Pydantic model or Zod schema. This checks: every required field exists; every value is the right type; nothing unexpected snuck in.
- Branch on the outcomeValid → consume downstream. Invalid → raise
ValidationErrorwith the specific field that failed (e.g. "email: expected str, got None") — gold for the retry loop in the next concept.
Three checks, one flow
# Three independent layers. Each catches # a different class of failure. DEFINE schema ContactInfo: name : string # required email : string # required, must look like email age : int # optional, range 0..130 FUNCTION validate(raw_output): # Layer 1 already done by tool_use # Layer 2: syntactic TRY: parsed = JSON.parse(raw_output) CATCH SyntaxError as e: RAISE "bad JSON: " + e.message # Layer 3: semantic TRY: contact = ContactInfo(**parsed) CATCH ValidationError as e: RAISE "bad shape: " + e.detail # e.detail = "email: expected str, got None" RETURN contact # typed, trusted
Misconceptions
JSON.parse() is enough."{"name": 123} parses cleanly even when name should be a string. Schema validation catches that. Always run both.age: 350 passes type check (it's an int) but is nonsense for a human. Add business-rule checks on top of schema checks.Three layers, near-zero failures. Tool use shapes the output, json.loads() catches syntax, Pydantic / Zod catches semantics.
The detailed error from layer 3 ("email: expected str, got None") is the single most valuable signal — it powers the self-correcting retry pattern in the next concept.
Failures are normal — recover gracefully
Even with three validation layers, parse failures will happen in production — ambiguous input, model variance, edge cases. The question isn't "if" but "how does your system respond?" The best agents aren't the ones that never fail. They're the ones that recover gracefully in the next turn.
The key insight: feed the specific validation error back into the next prompt. Claude reads the error, sees what went wrong, and self-corrects. Retry-with-error-feedback resolves ~90% of failures on the first retry, ~98% within three. Without it, blind retries usually repeat the same mistake.
The recalculating GPS
BEFORE: Early GPS units treated a missed turn as fatal. "Off route. Pull over. Restart navigation." The driver was stranded, the trip aborted, and someone had to manually re-enter the destination.
PAIN: That's exactly how naive code treats LLM parse failures — one missing comma kills the request, the user sees "something went wrong", and an engineer pages in to investigate why extract_contact() threw at 3 AM.
MAPPING: Modern GPS recalculates: detects you turned wrong, says "in 200 metres, make a U-turn", and offers a corrected path to the same destination. Retry-with-error-feedback does the same for Claude — tells it which turn it missed and lets it correct course on the very next attempt.
Five recovery strategies
- Retry with error feedbackAppend the exact validation error to the prompt and call again. Fixes ~90% of failures on the first retry.
- Fallback to a simpler schemaIf a 12-field nested schema keeps failing, try a 4-field flat one. Partial answer beats no answer.
- Partial parsingExtract whichever fields validated, flag the rest as incomplete, and let downstream decide.
- Cascading validatorsTry strict first, then progressively looser schemas. Handles "right data, slightly different shape" cases.
- Human-in-the-loopAfter all automated retries fail, route to a human reviewer. The safety net for genuinely ambiguous input.
Always wrap retries in exponential backoff (1s, 2s, 4s) and a max retry count (typically 3). Otherwise a permanently bad input loops forever, burning tokens.
Retry with the actual error
# Self-correcting loop. The error message is the # single most valuable thing to send back. FUNCTION extract_with_retry(text, max=3): last_error = NONE FOR attempt IN 1..max: prompt = "Extract contact from: " + text IF last_error: prompt += "\nPrevious attempt failed: " prompt += last_error prompt += "\nFix the output to match schema." TRY: raw = call_claude_with_tool(prompt) contact = VALIDATE(raw) RETURN contact # success CATCH ValidationError AS e: last_error = e.detail # keep specifics SLEEP(2 ** attempt) # 2s, 4s, 8s # exhausted retries: escalate RAISE "failed after " + max + " attempts"
Misconceptions
Recovery is a feature, not an afterthought. The error message from layer 3 validation is the magic ingredient — feed it back into the next prompt and Claude self-corrects.
Always cap retries (3 is plenty), use exponential backoff (2s, 4s, 8s), and have a human escalation path for the rare cases that survive automated recovery.
Agents read pictures and PDFs too
Real-world inputs aren't tidy plain text. Over 60% of business documents arrive as scanned images or PDFs — invoices, contracts, UCC filings, medical records, shipping labels. An agent that can only read clean text misses most of the data its users actually have.
Claude accepts images (JPEG, PNG, GIF, WebP) and PDFs natively in the same messages call — no separate OCR service. Combine multi-modal input with the tool-use pattern from cluster 2 and you get structured fields out of a scanned page in a single call.
The clerk vs the shoebox of receipts
BEFORE: Picture a clerk who can only read typed memos. Hand them a stack of typed reports and they're fast and accurate — the office runs smoothly.
PAIN: Then a customer drops off a shoebox of crumpled receipts, a photo of a whiteboard, and a 30-page scanned contract. The clerk has to rent an OCR machine, scan everything to text, hope the OCR didn't mangle the numbers, then start reading. Half the day is gone before any actual work begins.
MAPPING: A multi-modal agent is the clerk who learned to read pictures directly. Hand them the receipts, the whiteboard photo, the scanned contract — they look at each one and pull the fields out in one pass. No OCR pipeline, no intermediate text, no information lost in translation.
Image / PDF in, structured data out
- Encode the sourceRead the image or PDF as bytes, then base64-encode it. (Or upload to the Files API once and reference it by id for repeated reuse.)
- Build a multi-part messageThe
contentarray carries an image or document block (withmedia_type) plus a text block describing what to extract. - Attach the same toolReuse the cluster-2 pattern: define an extraction tool with
input_schema, force it viatool_choice. - Validate the resultThe tool_use input goes through the same Pydantic / Zod validator as text extraction. Vision is just another input source — the structured-output rails are unchanged.
Common uses: scanned UCC filings, invoice line items, photos of shipping labels, charts whose underlying numbers need to become JSON.
Extract from a scanned filing
# Vision + tool use = structured fields # pulled from a scanned page in one call. FUNCTION analyze_filing(image_bytes, media_type): image_b64 = BASE64(image_bytes) DEFINE tool "filing_record": input_schema: filing_number : string debtor_name : string secured_party : string filing_date : date SEND to Claude: content = [ { type: "image", source: { type: "base64", data: image_b64, media_type: media_type } }, { type: "text", text: "Extract the four fields from this scanned UCC filing." } ] tools : [filing_record] tool_choice : { type: "tool", name: "filing_record" } FOR block IN response.content: IF block.type == "tool_use": RETURN VALIDATE(block.input)
Misconceptions
Multi-modal isn't a bolt-on — it slots into the same pipeline. Image or PDF in, tool_use out, validator runs the same checks. Reuse the rails you built in clusters 2–4.
Over 60% of business documents are scans. An agent that handles them natively unlocks the messy reality of real input.
Quick Quiz
Tap any question to reveal the answer. One per concept.
if on Claude's answer, do you need structured output?tool_choice: { type: "tool", name: "..." } guarantee?tool_use block whose input matches the tool's JSON Schema. Shape is guaranteed — values are not. You still need a Pydantic / Zod check on top to catch hallucinated values that happen to be the right type.JSON.parse() alone enough?{"name": 123} parses cleanly even though name should be a string. Schema validation (Pydantic / Zod) is the second layer that confirms types, required fields, and ranges — the semantic shape your domain actually expects.Open the full module on desktop
The desktop version walks through a complete data-extraction pipeline in Python and Node.js: tool-use schema, Pydantic and Zod validators, self-correcting retry loop with exponential backoff, and a multi-modal vision example you can run locally.
Open M04 on Desktop → Full code · Pydantic + Zod · Retry lab · ~60 minModule 4 of 30 · Track 1: Foundations