What You Need to Know
Structured output is a reply from Claude in a fixed shape. Usually it is JSON that matches a schema. A schema lists the fields, their types and which ones are required. This sub-skill covers the point where that reply passes to the system that acts on it. A reply is well-formed when it parses and matches the shape. A reply is correct when its values are right.
This lesson uses one made-up example throughout: an online retailer whose engineering team uses Claude to convert customers' return requests into records in its order system. Each record holds an order ID, the refund amount, the number of damaged items and the condition of the product. A reply can parse cleanly and still be wrong: an order ID nobody issued, a refund amount of 90,000, an "unopened" condition with two damaged items.
Three Ways to Produce Structured Output
The three ways rank by who enforces the format.
JSON outputs are the strongest. You pass output_config.format with type json_schema and a schema object. The API then constrains decoding: while Claude writes the reply, the API allows only output that matches the schema. In the docs' words, "Structured outputs guarantee schema-compliant responses through constrained decoding". On a completed response, malformed JSON and missing required fields are no longer a class of bug that you handle. A refusal or a max_tokens cut can still break the schema. So check stop_reason first. This field says why Claude stopped.
import anthropic
from pydantic import BaseModel
class ReturnRecord(BaseModel):
order_id: str
refund_amount: float
damaged_items: int
condition: str
client = anthropic.Anthropic()
response = client.messages.parse(
model="claude-sonnet-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": return_request}],
output_format=ReturnRecord,
)
record = response.parsed_output # a ReturnRecord, already conformingStrict 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 pays the customer's refund 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 format. 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. It returns response.parsed_output. The helper still takes the Pydantic model as output_format and sends it as output_config.format for you. The first request with a new schema has extra latency, because the API compiles the schema into a grammar, the rules that constrained decoding follows. Compiled grammars stay in the cache for 24 hours from last use. Changing output_config.format invalidates the prompt cache for that conversation thread. A new field in the retailer's return schema brings both costs again. On a request built by hand, the parameter has changed. As of September 2026 the old spelling is deprecated: "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 customer comment on the return record would arrive last. If the order matters, mark every property required. Strict schemas also have complexity limits across the whole request: at most 20 strict tools, 24 optional parameters and 16 parameters with union types. A union type accepts more than one type. If the compiled grammar is still too large, the API returns a 400 with the message "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, and that subset is limited. Your validation still checks what the subset does not support. 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 (a reference to a schema in another file). Array constraints beyond minItems of 0 or 1 are unsupported too. additionalProperties must be set to false. The grammar guarantees that refund_amount in the code above is present and a number, but not that it falls between 1 and 500. So the refund amount of 90,000 passes the grammar.
The SDK helper, used in the code above, is an exception. The SDK removes the constraints that it cannot send in the request. It appends them to the property description, then checks them itself. The SDK validates minimum, maximum, multipleOf, minLength and maxLength. A Pydantic ge/le is enforced client-side, even though the grammar never saw it. No layer enforces cross-field consistency or referential existence (whether an identifier names a real thing). You must write those checks whichever path you take. If you build the request by hand rather than through parse(), you must write the range check too.
Dated current state, 30 September 2026
The parameter names, header strings, cache windows, limits and SDK exceptions above are current as of this date. They have changed 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 are in the domain quick reference, where a rename is a one-line fix.
Your validation layer handles everything that the schema cannot express. It does this in four checks.
- Structural. Required fields present, types correct, enums inside their declared sets. This check is cheap, and needs little extra code if you parse the reply into a typed model. One exception applies as of September 2026. Structured outputs do not guarantee the capitalisation of
enumandconstvalues, in JSON outputs or in strict tool use. A condition can arrive as "Unopened" where the schema says "unopened", so compare enum values case-insensitively. A typed model with a case-sensitive enum will reject a response that 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 refund amount of 90,000 fails here.
- Cross-field consistency. This check catches what a per-field check never sees. Does a total equal the sum of its parts? Is an end date after a start date? Is a status compatible with the fields that accompany it? An "unopened" condition with two damaged items fails here.
- Referential. Any identifier naming a real thing, checked against the system that issues it. The order ID is checked against the order system. This is the only check that catches a confidently invented identifier.
def validate(record: ReturnRecord, known_order_ids: set[str]) -> None:
if not 1.0 <= record.refund_amount <= 500.0:
raise ValueError(f"refund_amount out of range: {record.refund_amount}")
if record.condition not in {"unopened", "used", "damaged"}:
raise ValueError(f"unknown condition: {record.condition}")
if record.condition == "unopened" and record.damaged_items > 0:
raise ValueError("unopened condition is inconsistent with a non-zero damaged item count")
if record.order_id not in known_order_ids:
raise ValueError(f"order_id does not exist in the order system: {record.order_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 visible failure into a wrong record that nobody notices. Someone would have fixed the visible failure, but the wrong record gets trusted for months. A missing condition filled with "unopened" makes a return record look complete.
Check stop_reason Before the Payload
Defensive parsing means that you check a response before you trust it. 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 value, compaction, which 6.1 covers. 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. The response carries a stop_details object that names the policy category. An integration that branches only on HTTP status sees a success with no useful content.
def take_record(response) -> ReturnRecord:
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_outputTruncation is a defensive-parsing concern, not an error-handling one, because a truncated response can be syntactically valid up to the point where it stops. A JSON object stopped after its fourth field still parses if your parser is lenient. A naive extractor then gives the downstream system a record whose last field was never generated. For the retailer, the condition 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 includes both. Use a structured-output feature where one exists. Without one, a model asked for JSON in the prompt often reasons through the problem 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 without an error. An order 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 code can detect that Claude declined, and Claude does not write a paragraph of vague doubt. For a return request that never states the condition, Claude returns that phrase. For long documents (over about 20k tokens), ask Claude to extract word-for-word quotes first, before it does the task. This step focuses attention and leaves you a string that you can search for in the source. Make each claim carry a supporting quote. 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 sign of fabrication.
Where the answer comes from documents that you supply, the API's Citations feature does more 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 work with structured outputs. A request that enables citations and sends output_config.format 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 check outside the model rather than a better prompt. Check anything that names a real thing against the system that issues it. Check high-consequence output 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 option. It is another call that draws from the same set of likely answers. 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 retailer's retry names the refund amount of 90,000.
Cap the attempts at a small number and make the cap explicit. An uncapped retry loop on a systematic failure causes unplanned cost rather than making the system more resilient. 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 condition failing on most return requests may mean customers never state one.
In the exam
The exam often describes 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 covers how to build the checks on Claude's reply before anything fails: the enforced schema, the validator, the stop-reason check, the retry policy. Once something has failed, Domain 4's Debugging and Error Handling covers two tasks. One is to find which of the schema, prompt or parser is at fault. The other is to choose the recovery.
Exam Traps
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 and maxLength, so a conforming response can still carry an out-of-range number, inconsistent fields or an invented identifier. Constrained decoding removes parse failures on a completed response and nothing else. Keep the range, consistency and identity checks.
Diagnosing a record whose final field is empty or defaulted as a schema or prompt problem
Check stop_reason first. Both max_tokens and model_context_window_exceeded mean the response is truncated, and a lenient parser still accepts it up to the cut. If the response is complete and the same field fails across many inputs, look at the schema, the prompt or the validator.
Treating a refusal as an empty success, and answering it with retry-on-HTTP-error handling
A refusal arrives as a normal HTTP 200, so status-code retry logic never sees it. Branch on stop_reason before parsing, treat refusal as its own outcome and read the stop_details object that names the policy category.
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 is self-report, not a check. Re-ask with the specific validation errors attached, cap the attempts, and check identifiers against the system that issues them.
Practice Scenario
An HR software company feeds Claude's summaries of employee expense claims into its product database. The prompt asks for JSON and gives a worked example; the integration parses the response and writes the record. Roughly one claim in fifty fails to parse at all, and the team wants that class of failure gone before they add a second customer. Which change removes it, and what still has to be checked afterwards?
Build Exercise
Enforce the Shape, Then Try to Break It
What you'll learn
- How the three ways of producing structured output differ in who enforces the shape
- Which errors a guaranteed schema does not prevent, so that they need a validator
- Why stop_reason is checked before the payload
- How a retry that carries the validation errors differs from an identical retry
- 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 the schema could not express.
- 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 shows it with numbers rather than as a rule to accept.
You should see: A non-zero failure count for the instruction version and zero for both enforced versions on every completed response.
- 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 shows the main point of this sub-skill with your own data.
You should see: At least one conforming response failing validation, most often on range or referential grounds.
- 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 you remember to check stop_reason before parsing.
You should see: A response carrying stop_reason max_tokens whose JSON still looks plausible up to the point where it stops.
- 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 a failure that code which branches on HTTP status cannot detect.
You should see: An HTTP 200 with stop_reason refusal and a stop_details object naming the policy category.
- 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: A retry that carries the validation errors 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 from adding cost without limit.
- 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 record was written
Why: Seeing a plausible invented identifier next to a real one is the fastest way to stop trusting output because it reads well.
You should see: A precisely formatted, entirely invented reference that every structural check passes.
Sources
- Structured outputs — Claude Docs, output_config.format, strict tool use, the constrained-decoding guarantee, the unsupported schema subset, enum casing, property ordering, complexity limits and the output_format deprecation
- Citations — Claude Docs, API-extracted cited_text and the 400 when citations meet structured outputs
- Stop reasons and fallback — Claude Docs, the seven stop reasons, truncation and refusal as an HTTP 200
- Prompting Claude Sonnet 5.5 — Claude Docs, reasoning tasks with JSON output and the last-value parse when structured outputs are unavailable
- Using the Messages API — Claude Docs, response structure and content blocks
- Reduce hallucinations — Claude Docs, permission to decline, quote grounding, citation verification and best-of-N
- Increase output consistency — Claude Docs, when to move from prompt techniques to structured outputs
- Claude Certified Developer Foundations Exam Guide — Domain 6, Output Handling — Anthropic