Skip to content
Domain 6Task 6.3

Output Handling

Learn this interactively

What You Need to Know

Structured output is a reply from Claude in a fixed shape, usually JSON that matches a schema: the fields, their types and which ones are required. This sub-skill covers the boundary between that reply and the system that acts on it. A reply is well-formed when it parses and matches the shape, and correct when its values are right.

A driving school uses Claude to turn instructors' lesson notes into records in its booking system. Each record holds a learner ID, the lesson length in minutes, the manoeuvres covered and a progress grade. A reply can parse cleanly and still be wrong: a learner ID nobody issued, a 900-minute lesson, a "test ready" grade with no manoeuvres listed.

Three Ways to Produce Structured Output

The three ways rank by who enforces the shape.

JSON outputs are the strongest. You pass output_config.format with type json_schema and a schema object, and the API constrains decoding. In the docs' words, "Structured outputs guarantee schema-compliant responses through constrained decoding". On a completed response, malformed JSON and missing required fields stop being a class of bug you handle. A refusal or a max_tokens cut can still break the schema. So stop_reason, the field that says why Claude stopped, comes first.

python
import anthropic
from pydantic import BaseModel

class SlabRecord(BaseModel):
    slab_id: str
    thickness_mm: float
    flaw_count: int
    finish_grade: str

client = anthropic.Anthropic()

response = client.messages.parse(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": inspection_note}],
    output_format=SlabRecord,
)

record = response.parsed_output  # a SlabRecord, already conforming

Strict tool use applies the same constrained decoding to tool names and tool inputs. You set strict to true on a tool definition. Use it when the structured value is an action's arguments rather than the response itself. A tool that books a learner's next lesson is one case. The two features can be used in one request.

Instruction-plus-example JSON is the fallback where neither is available. You describe the schema and show one worked example of the exact shape. You are then relying on the model honouring a request, not on an API constraint. Current docs say that if you need Claude to always output valid JSON conforming to a schema, you should use structured outputs instead of the prompt-engineering techniques.

The Python SDK helper is client.messages.parse(...) with a Pydantic model, and it returns response.parsed_output. The helper still takes the model as output_format and sends it as output_config.format for you. The first request with a new schema pays grammar-compilation latency. Compiled grammars cache for 24 hours from last use. Changing output_config.format invalidates the prompt cache for that conversation thread. Adding a field to the driving school's lesson schema pays both costs again. On a raw request the parameter has moved. As of September 2026 the old spelling is on its way out: "The output_format parameter is deprecated and will be removed in the future. To use it anyway, add the structured-outputs-2025-11-13 beta header. Without it, the API returns a 400 error."

As of September 2026, property order follows the schema, except that required properties come first and optional ones after. A consumer that streams the reply or reads fields by position sees them in that order. An optional instructor comment on the lesson record would arrive last. If the order matters, mark every property required. Strict schemas also carry complexity limits across the whole request: at most 20 strict tools, 24 optional parameters and 16 parameters with union types. If the compiled grammar is still too large, the API returns a 400 reading "Schema is too complex for compilation".

The Schema Is Not the Validator

Parsing answers "is this well-formed?". Validation answers "is this right?". Structured outputs settle the first, which is why the second gets forgotten.

The grammar enforces only the supported JSON Schema subset, which is limited, and the gaps are where validation still lives. The subset does not support numerical constraints (minimum, maximum, multipleOf) or string constraints (minLength, maxLength). It also excludes recursive schemas, complex types within enums and external $ref. Array constraints beyond minItems of 0 or 1 are unsupported too. additionalProperties must be set to false. The grammar guarantees that thickness_mm in the code above is present and a number, but not that it falls between 8 and 40. The 900-minute lesson passes the grammar for the same reason.

The SDK helper, used in the code above, is an exception. The SDK strips the constraints it cannot put on the wire, appends them to the property description, then checks them itself. minimum, maximum, multipleOf, minLength and maxLength are SDK-validated. A Pydantic ge/le is enforced client-side, even though the grammar never saw it. No layer enforces cross-field consistency or referential existence; those are yours whichever path you take. If you build the request by hand rather than through parse(), the range check is yours too.

Dated current state, 30 September 2026

The parameter names, header strings, cache windows, limits and SDK carve-outs above are current as of this date and have changed shape between releases. output_format became output_config.format, and the structured-outputs-2025-11-13 beta header became unnecessary for it. The old spelling now returns a 400 unless that header is sent. Remember the mechanism: constrained decoding guarantees shape and nothing else. The exact strings live in the domain quick reference, where a rename is a one-line fix.

Everything the schema cannot express is your validation layer's job, in four checks.

  • Structural. Required fields present, types correct, enums inside their declared sets. Cheap, and mostly free if you round-trip through a typed model. One exception: as of September 2026 structured outputs do not guarantee the capitalisation of enum and const values, in JSON outputs or in strict tool use. A grade can come back as "Test ready" where the schema says "test ready", so compare enum values case-insensitively. A typed model with a case-sensitive enum will reject a response the schema accepted.
  • Range and plausibility. Numbers inside the bounds the domain allows, dates inside the window the process can produce, strings matching the format the downstream system requires. The 900-minute lesson fails here.
  • Cross-field consistency. Catches what a per-field check never sees: a total equal to the sum of its parts, an end date after a start date, a status compatible with the fields that accompany it. A "test ready" grade with no manoeuvres listed fails here.
  • Referential. Any identifier naming a real thing, checked against the system that issues it. The learner ID gets looked up in the booking system. The only check that catches a confidently invented one.
python
def validate(record: SlabRecord, known_slab_ids: set[str]) -> None:
    if not 8.0 <= record.thickness_mm <= 40.0:
        raise ValueError(f"thickness_mm out of range: {record.thickness_mm}")
    if record.finish_grade not in {"honed", "polished", "flamed"}:
        raise ValueError(f"unknown finish_grade: {record.finish_grade}")
    if record.finish_grade == "polished" and record.flaw_count > 0:
        raise ValueError("polished grade is inconsistent with a non-zero flaw count")
    if record.slab_id not in known_slab_ids:
        raise ValueError(f"slab_id does not exist in the yard register: {record.slab_id}")

Write the validator as code that runs on every response, not as a paragraph in a design document. Give it one behaviour on failure: raise, with the raw response body and the request id attached. Argue against any branch that substitutes a default. A default turns a loud failure, which someone would have fixed, into a quiet wrong record that gets trusted for months. A missing grade filled with "beginner" makes a learner's file look complete.

Check stop_reason Before the Payload

Defensive parsing, checking a response before trusting it, starts with the stop reason, not with the content blocks. The seven documented values are end_turn, max_tokens, stop_sequence, tool_use, pause_turn, refusal and model_context_window_exceeded. A compaction request adds an eighth, compaction, covered in 6.1. Three of the values mean the content you are about to parse is not what you asked for.

max_tokens means the response reached your max_tokens limit, so it is truncated. model_context_window_exceeded means the response filled the model's context window. The documented instruction is to treat that response as truncated. refusal means Claude declined, and it arrives as a normal HTTP 200 rather than an error. It carries a stop_details object naming the policy category. An integration that branches only on HTTP status sees a success with no useful content.

python
def take_record(response) -> SlabRecord:
    if response.stop_reason == "refusal":
        raise RefusedError(response.stop_details, response.id)
    if response.stop_reason in ("max_tokens", "model_context_window_exceeded"):
        raise TruncatedError(response.stop_reason, response.id)
    if response.stop_reason != "end_turn":
        raise UnexpectedStopError(response.stop_reason, response.id)
    return response.parsed_output

Truncation is a defensive-parsing concern, not an error-handling one, because a truncated response can be syntactically valid up to the cut. A JSON object stopped after its fourth field still parses if your parser is lenient. A naive extractor then hands the downstream system a record whose last field was never generated. For the driving school, the progress grade goes missing.

Never take everything from the first brace to the last

Taking everything between the first and last braces looks defensive and is the opposite. It succeeds on truncated output, on output wrapped in prose and on output followed by commentary. It turns three distinct failures into one silently wrong record. When the model writes a draft before its final JSON, the range swallows both. Use the structured-output channel where it exists. Without one, a model asked for JSON in the prompt often works the problem out in the response text and writes the JSON at the end. As of September 2026 the Claude Sonnet 5.5 prompting guide documents a parse for that case. It reads only the text blocks and treats a max_tokens stop as failed. It tries to parse a JSON value from each opening brace or bracket and keeps the last complete value. Then it checks that the value has the expected fields, and retries once if it does not.

Scepticism Towards Confident Output

Tone carries no information about accuracy. The system produces fluent, well-formatted, suitably cautious output whether or not the standard it cites exists. A validator that only checks types passes an invented reference straight through. A learner ID can have the right format and not exist.

The reduce-hallucinations guide documents these controls. Give the model permission to decline: "Allow Claude to say 'I don't know': Explicitly give Claude permission to admit uncertainty. This simple technique can drastically reduce false information". Then give it the exact wording to use, so the abstention is machine-detectable, not a paragraph of vague doubt. A note that never names the manoeuvres gets that phrase back. For long documents (over about 20k tokens), ask Claude to extract word-for-word quotes first, before performing the task. This focuses attention and leaves you a string you can search for in the source. Make each claim carry a supporting quote, and instruct that a claim without one is removed rather than kept. Restrict the answer to the supplied material when that is the intent, rather than assuming it. Best-of-N verification runs the same prompt several times and treats disagreement between runs as a fabrication signal.

Where the answer comes from documents you supply, the API's Citations feature goes further than prompting for quotes. As of September 2026 you set citations.enabled on each document. The response carries cited_text that the API extracts itself, so "citations are guaranteed to contain valid pointers to the provided documents". Citations do not combine with structured outputs. Enabling citations and sending output_config.format in the same request returns a 400.

That guide closes with its own caveat: "while these techniques significantly reduce hallucinations, they don't eliminate them entirely. Always validate critical information, especially for high-stakes decisions." The engineering answer is a boundary rather than a better prompt. Anything with a real-world referent gets checked against the system that issues it. High-consequence output goes through a check before it becomes an action.

When Validation Fails

A failed validation is information, and the next request should use it. An identical re-ask is the weakest move: another call sampling the same distribution. If the schema or the prompt is at fault, it fails the same way indefinitely. Re-ask with the specific validation errors attached: which field was missing, which value fell outside its range, which two fields disagreed. The second attempt now has something the first did not. The driving school's retry names the 900-minute length.

Cap the attempts at a small number and make the cap explicit. An uncapped retry loop on a systematic failure is a cost incident rather than a resilience feature. Treat repeated failure as a defect, not bad luck. If the same field fails across many inputs, one of three things is wrong. The schema asks for something the source material does not contain. The prompt has not said where to find it. Or the validator asserts something the domain does not guarantee. Log enough to tell which: the raw response, the stop reason, the usage block, the request id, the validation error and the input that produced it. A grade failing on most lesson notes may mean instructors never write one.

In the exam

The exam's recurring construction is an integration that parses successfully and still writes a wrong record. It tests the difference between well-formed and correct.

The Domain 4 boundary

This sub-skill owns building the boundary before anything fails: the enforced schema, the validator, the stop-reason check, the retry policy. Working out which of the schema, prompt or parser is at fault once it has failed, and choosing the recovery, is Domain 4's Debugging and Error Handling.

Exam Traps

Exam Trap

Adopting structured outputs and deleting the application validation code, on the grounds that the shape is now guaranteed.

The supported subset excludes minimum, maximum, minLength, maxLength, recursion and external refs, so a conforming response can still carry an out-of-range number, an inconsistent pair of fields or an invented identifier. Constrained decoding removes the parse-failure class on a completed response and nothing else.

Exam Trap

Reading a record whose final field is empty or defaulted as a schema or prompt problem.

That is a stop_reason question. Both max_tokens and model_context_window_exceeded mean the response is truncated, and a lenient parser will still accept it up to the cut. Options that blame the schema diagnose the wrong layer.

Exam Trap

Treating a refusal as an empty success, and answering it with retry-on-HTTP-error handling.

A refusal arrives as a normal HTTP 200 carrying a stop_details object that names the policy category. Status-code branching never sees it, so retry logic keyed to HTTP errors cannot address it.

Exam Trap

Answering a validation failure with an identical retry, or by asking the same call to check its own work.

Re-sending the same request samples the same distribution again, and self-marking substitutes self-report for a check against an authoritative source. The keyed moves attach the specific errors, cap the attempts, and verify referents against the system that issues them.

Practice Scenario

A marble-finishing workshop feeds Claude's per-slab inspection summaries into its production database. The prompt asks for JSON and gives a worked example; the integration parses the response and writes the record. Roughly one slab in fifty fails to parse at all, and the team wants that class of failure gone before they add a second line. Which change removes it, and what still has to be checked afterwards?

Build Exercise

Build Exercise

Enforce the Shape, Then Try to Break It

Difficulty
45 minutes

What you'll learn

  • How the three ways of producing structured output differ in who enforces the shape
  • Which errors survive a guaranteed schema and therefore need a validator
  • Why stop_reason is checked before the payload
  • How a retry that carries the validation errors differs from an identical retry
  1. Take one extraction task you run today and write the schema you actually need, including the ranges, enums and cross-field rules the database depends on

    Why: Writing the real rules first shows you immediately how many of them a JSON schema cannot express.

    You should see: A schema plus a separate list of rules that had nowhere to go in it.

  2. Implement it three ways against the same ten inputs: instruction-plus-example JSON, strict tool use with strict set to true, and output_config.format with type json_schema. Count parse failures for each

    Why: The ranking is about who enforces the shape, and counting failures makes that concrete rather than doctrinal.

    You should see: A non-zero failure count for the instruction version and zero for both enforced versions on every completed response.

  3. Write the validator as code, asserting required fields, types, enums, numeric ranges, cross-field consistency, and one identifier checked against the system that issues it. Confirm it rejects at least one response the schema accepted

    Why: A rejection the schema let through is the whole argument of this sub-skill, produced from your own data.

    You should see: At least one conforming response failing validation, most often on range or referential grounds.

  4. Set max_tokens deliberately low on one run so a record truncates, then check stop_reason before parsing and confirm your pipeline refuses the record rather than defaulting its last field

    Why: Watching a truncated payload parse cleanly is what makes the stop-reason-first rule stick.

    You should see: A response carrying stop_reason max_tokens whose JSON still looks plausible up to the cut.

  5. Trigger a refusal on purpose and confirm your integration does not treat the 200 as an empty success. Read stop_details and decide what your application should do with it

    Why: A refusal is the one failure that never looks like one to status-code branching.

    You should see: An HTTP 200 with stop_reason refusal and a stop_details object naming the policy category.

  6. Add a retry that attaches the specific validation errors to the follow-up request, cap it at two attempts, and log the raw body, stop_reason, usage and request id on every failure

    Why: An informed retry and an identical one look the same in a diagram and behave completely differently in production.

    You should see: Second attempts that fix the named field, and a cap that stops a systematic failure becoming a cost incident.

  7. Finally, ask for a reference number the source material cannot support, watch how confident the fabrication looks, and write the check that would have caught it before the write

    Why: Seeing a plausible invented identifier next to a real one is the fastest cure for trusting fluent output.

    You should see: A precisely formatted, entirely invented reference that every structural check passes.

Sources