Module 4 of 30
Structured
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.

Track 1: Foundations ⏱ ~15 min read 4 / 30
Concept 1 of 5

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."

Concept 1 of 5

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.

Concept 1 of 5

From text to action

  1. Claude generatesThe model produces output. Free-form by default; structured if you constrain it.
  2. Your code consumesA parser turns the output into native objects: dicts, classes, records.
  3. Downstream actsThose objects feed a DB insert, an API call, a UI render, or a branch decision.
  4. One bad shape breaks everythingIf field names drift or types mismatch, every consumer downstream fails — often silently.
Concept 1 of 5

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.

Concept 1 of 5

Misconceptions

"Structured output is only for data extraction."
It's also essential for routing decisions ("escalate vs reply"), tool selection, and any branch your code takes on Claude's answer. Anywhere downstream code does an if on Claude's output, you need structure.
"A 5% parse failure rate is fine."
At 10,000 requests a day that's 500 broken requests — each one a customer error, lost write, or silent data corruption. Tool-use extraction drops failure under 0.5%, often below 0.1%. The 10x is worth it.

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.

Concept 2 of 5

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%.

Concept 2 of 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.

Concept 2 of 5

The four-step pattern

  1. Define a toolGive it a name, a description, and an input_schema — the exact JSON Schema describing the shape you want back.
  2. Force its useSet tool_choice: { type: "tool", name: "your_tool" }. Claude can no longer respond with plain text.
  3. Read the blockLoop through the response content blocks. Find the one with type == "tool_use". Its input field is your typed data.
  4. Skip executionFor pure structured output you never call any function. The "tool" was just a schema. Claude's stop_reason will be tool_use and 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.

Concept 2 of 5

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"
Concept 2 of 5

Misconceptions

"Tool use is only for calling external functions."
It's also Claude's most reliable structured-output mechanism, even when you never execute the tool. The "tool" is just a typed form — you read its input and discard the rest.
"tool_use guarantees the values are correct."
It guarantees the JSON shape — right keys, right types. The values can still be hallucinated. "jane@example.com" may not be Jane's real email. Always layer semantic checks on top.

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.

Concept 3 of 5

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.

Concept 3 of 5

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.

Concept 3 of 5

The validation pipeline

  1. 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.)
  2. Parse the JSONRun json.loads() / JSON.parse(). This catches syntax errors — missing commas, mismatched braces, escape glitches.
  3. 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.
  4. Branch on the outcomeValid → consume downstream. Invalid → raise ValidationError with the specific field that failed (e.g. "email: expected str, got None") — gold for the retry loop in the next concept.
Concept 3 of 5

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
Concept 3 of 5

Misconceptions

"JSON.parse() is enough."
It only checks syntax. {"name": 123} parses cleanly even when name should be a string. Schema validation catches that. Always run both.
"Schema validation catches all bad output."
It catches type errors and missing fields, not logical ones. 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.

Concept 4 of 5

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.

Concept 4 of 5

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.

Concept 4 of 5

Five recovery strategies

  1. Retry with error feedbackAppend the exact validation error to the prompt and call again. Fixes ~90% of failures on the first retry.
  2. Fallback to a simpler schemaIf a 12-field nested schema keeps failing, try a 4-field flat one. Partial answer beats no answer.
  3. Partial parsingExtract whichever fields validated, flag the rest as incomplete, and let downstream decide.
  4. Cascading validatorsTry strict first, then progressively looser schemas. Handles "right data, slightly different shape" cases.
  5. 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.

Concept 4 of 5

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"
Concept 4 of 5

Misconceptions

"If parsing fails, just retry the same prompt."
Blind retries often repeat the same mistake — same prompt, same answer. Include the specific error ("field 'email' expected str, got None") so Claude can self-correct.
"Retry forever until it works."
A genuinely bad input (gibberish text, mismatched task) will fail every attempt. Without a max-retries cap and exponential backoff you burn tokens, hit rate limits, and amplify outages.

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.

Concept 5 of 5

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.

Concept 5 of 5

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.

Concept 5 of 5

Image / PDF in, structured data out

  1. 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.)
  2. Build a multi-part messageThe content array carries an image or document block (with media_type) plus a text block describing what to extract.
  3. Attach the same toolReuse the cluster-2 pattern: define an extraction tool with input_schema, force it via tool_choice.
  4. 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.

Concept 5 of 5

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)
Concept 5 of 5

Misconceptions

"You still need a separate OCR step before Claude."
No — Claude reads the image or PDF natively. Adding an OCR pipeline in front loses layout cues (tables, signatures, stamps) that the model would otherwise use.
"Files API is always cheaper than base64."
Both put the bytes in your context window every call. Files API saves bandwidth and re-encoding only when the same doc is referenced repeatedly. For one-off extracts, base64 is simpler. For repeated reads, combine Files API with prompt caching for the real win.

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.

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 min

Module 4 of 30 · Track 1: Foundations