Claude API Structured Output: Constraining Responses, Validating Tool Inputs, and What Replaced Prefill
There are two different problems people mean by "Claude API structured output," and they have two different mechanisms. One is constraining the shape of the model's response. The other is guaranteeing that the arguments Claude passes to your tool validate against your schema. Reaching for the wrong one — or for a technique that has since been removed from the API — accounts for most of the frustration in this area.
Constraining the response: output_config.format
To make the response itself conform to a schema, pass output_config with a format object on messages.create(). This is a parameter on the regular Messages API — there is no separate endpoint and no beta header for the general case.
The recommended path is client.messages.parse(), which sends the request and validates the response against your schema for you, rather than handing you a string you then have to parse and check yourself.
One correction worth making explicitly, because a great deal of older example code still shows it: the top-level output_format parameter is deprecated. Use output_config: {format: {...}} instead. This is a general API change, not something specific to any one model generation.
One incompatibility to know about up front: structured outputs cannot be combined with citations. If you set citations: {enabled: true} on document blocks and also request a response format, the request returns a 400.
Validating tool inputs: strict
The second mechanism is strict: true, and it solves a different problem — it guarantees that the input object on a tool_use block validates exactly against the tool's schema.
Two details cause most of the failures here:
strict goes on the tool definition, not on tool_choice. It is a top-level field alongside name, description, and input_schema. Putting it on tool_choice does nothing.
The schema has requirements. It must set additionalProperties: false and must have a required array. A schema missing these will not satisfy strict mode.
Strict mode is not compatible with everything. It cannot be combined with programmatic tool calling, with disable_parallel_tool_use, with a forced tool_choice, or with MCP tools. If you need one of those, you need a different approach to reliability — usually structured outputs on the response, or validation on your side of the boundary.
Always parse tool inputs, never string-match them
This one is easy to get away with for a long time and then breaks in production.
The current models — Fable 5 and 5.1, Opus 5, and the 4.6 / 4.7 / 4.8 family — may produce different JSON string escaping inside tool call input fields, including Unicode escapes and escaped forward slashes. The value is correct; its serialized form is not stable across models.
So parse tool inputs with a real JSON parser — json.loads() in Python, JSON.parse() in JavaScript — and read fields off the resulting object. Any code that does substring matching or regex extraction against the serialized input is depending on an escaping detail that is explicitly allowed to change, and it will fail on a model swap with no other symptom.
What replaced the prefill trick
The standard old technique for forcing JSON was assistant prefill: end the messages array with an assistant turn containing an opening brace, so the model had no choice but to continue inside a JSON object.
That technique now returns a 400. Assistant message prefills are removed on Fable 5 and 5.1, Opus 5, Sonnet 5, and Opus 4.6, 4.7 and 4.8, and on Sonnet 4.6. It is not deprecated-but-working; it is an error.
The replacements, in order of directness:
- Structured outputs —
output_config.format. This is what the prefill trick was approximating, done properly. - System prompt instructions describing the required shape, when you want a format the schema mechanism does not express well.
There is a related removal worth knowing if you used forced tool calls as a JSON-extraction trick: on Claude Fable 5.1 and Claude Mythos 5.1, tool_choice: {type: "any"} and {type: "tool", name: ...} return a 400, including on token counting and the Batches API. If a forced tool call existed only to get structured data back, structured outputs is the direct replacement. If you genuinely want the model to call a specific tool, use tool_choice: {type: "auto"} with an explicit instruction naming the tool, and add strict: true to keep the arguments schema-valid. {type: "none"} is unaffected, and disable_parallel_tool_use still works with auto.
Choosing between the two mechanisms
Ask what you are going to do with the result.
Use structured outputs when the model's answer is the data. Classification, extraction, scoring, anything where you want to deserialize the response into a typed object and move on. client.messages.parse() gives you validation as part of the call.
Use strict tools when the model is calling into your code. The schema is your function signature, and strict mode is what stops you from writing defensive parsing at every call site.
Use both in an agent that does structured work and also returns a structured final answer. They operate on different parts of the exchange and do not conflict.
If your reason for wanting structure is "the model sometimes wraps JSON in prose" or "sometimes it adds a markdown fence," that is exactly what structured outputs is for. Do not solve it with a stricter prompt and a regex.
A note on schema design
The schema is not just validation — it is instruction. The model reads field names and descriptions, so a field called t with no description produces worse results than one called sentiment with a one-line description of what belongs in it, even though both validate identically.
Keep schemas as flat as the data allows. Deep nesting and long unions are harder for the model to fill correctly and harder for you to debug when a field comes back empty. And keep required honest: marking every field required forces the model to invent values for things it has no basis for, which is worse than an absent field you can detect.
Frequently asked questions
How do I get JSON output from the Claude API?
Pass output_config with a format object on messages.create(), or use client.messages.parse() to send and validate in one step. The older top-level output_format parameter is deprecated.
Can I still prefill an assistant turn to force JSON?
No. Assistant prefills return a 400 on all current models. Structured outputs replaces the technique, with system prompt instructions as the fallback for formats a schema does not express well.
Where does strict go — on the tool or on tool_choice?
On the tool definition, as a top-level field next to name, description, and input_schema. The schema must also set additionalProperties: false and include a required array.
Why do my tool inputs look different across models?
JSON string escaping inside tool call inputs varies between models — Unicode escapes and escaped forward slashes are both allowed. Parse the input with a JSON parser rather than matching on the serialized string.
Can I use structured outputs together with citations?
No. Combining citations: {enabled: true} on document blocks with a requested output format returns a 400. Pick one per request.
Why did tool_choice: any stop working?
Forced tool use was removed on Claude Fable 5.1 and Claude Mythos 5.1 — both any and tool return a 400 there. Use auto plus an instruction naming the tool, add strict: true for schema-valid arguments, or switch to structured outputs if the forced call was only a way to get JSON.
The short version
Two mechanisms, two jobs: output_config.format constrains the response, strict: true on the tool definition constrains tool arguments. Prefer client.messages.parse() for the first and remember that output_format is the deprecated spelling. Prefill is gone — it returns a 400, not a warning — and forced tool choice is gone on the newest models, so structured outputs is now the direct answer to "I need JSON back." Parse tool inputs with a JSON parser, never a regex. And treat the schema as a prompt: descriptive field names, flat where possible, and required only where you would genuinely rather have a wrong value than a missing one.