Claude Structured Output: JSON Schema Guide and What Still Fails
Put a JSON schema in output_config.format and Claude returns matching JSON in the text block. Still check stop_reason and validate ranges and lengths yourself.
On this page

Yes, Claude has structured outputs, and as of October 30, 2026 they are generally available on the Claude API, Amazon Bedrock, Google Cloud and Microsoft Foundry. Add an output_config.format object with "type": "json_schema" and your schema to a Messages API request, and Claude returns JSON that matches it inside the response's text block. When what you need is a valid function call rather than a final answer, set strict: true on the tool definition instead, or use both in one request. Claude has no separate "JSON mode", and the old top-level output_format field with the structured-outputs-2025-11-13 beta header is deprecated.
The guarantee covers the shape of the JSON, not every rule you can write in a schema. Check stop_reason before you parse, because a refusal or a max_tokens cut still comes back as HTTP 200 with output that may not match. Keyword support is partial too: minimum is rejected with a 400, and in a test through LaoZhang API on October 30, 2026, a minLength/maxLength rule was accepted and then ignored, so ranges and lengths belong in your own validation step.
Disclosure: laozhang.ai is our own service. Its prices are platform pricing, not an independent comparison.
If you call Claude through a proxy or gateway, also confirm that the schema arrived, because a dropped field doesn't always raise an error. In the same tests, a request using the legacy output_format field came back as an ordinary prose answer with HTTP 200 and no extra input tokens, not as an error.
Send a JSON schema in output_config.format
A working request needs three things on top of a normal Messages API call: a supported model, an output_config.format object whose type is json_schema, and a schema in which every object sets additionalProperties: false. No beta header is required. This request asks Claude Haiku 5.5 to pull contact details out of an email:
curl https://api.anthropic.com/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-haiku-5-5",
"max_tokens": 1024,
"messages": [{
"role": "user",
"content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm."
}],
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"plan_interest": {"type": "string"},
"demo_requested": {"type": "boolean"}
},
"required": ["name", "email", "plan_interest", "demo_requested"],
"additionalProperties": false
}
}
}
}'The JSON arrives as a string in the first block of content whose type is text, with stop_reason: "end_turn", so you still run json.loads() or JSON.parse() on it unless an SDK helper does that for you. When this exact body was sent on October 30, 2026 to LaoZhang API's Anthropic-compatible endpoint, https://api.laozhang.ai/v1/messages, Claude Haiku 5.5 answered in 3.61 seconds with this text block:
Disclosure: laozhang.ai is our own service. Its prices are platform pricing, not an independent comparison.
{"demo_requested": true, "email": "john@example.com", "name": "John Smith", "plan_interest": "Enterprise"}Every observed result below comes from that endpoint on that date, mostly with claude-haiku-5-5 and one request on claude-sonnet-5-5. LaoZhang's own page on Claude 400 ValidationException errors through its AWS channel says some of its Claude traffic runs through AWS and Bedrock channels that validate request bodies strictly, and the response doesn't show which channel served a call. Where those results differ from Anthropic's structured outputs documentation, both are given below, and statements about the direct api.anthropic.com behavior come from that documentation.
Python and Pydantic: messages.parse() checks what the schema can't
In Python, pass a Pydantic model to client.messages.parse() as output_format. The SDK converts the model into output_config.format, sends the request, validates the reply against your model and returns it in parsed_output. Here output_format is the current name of the helper's argument, not the deprecated API field, so this code is up to date with the anthropic package 2.3.0 and pydantic 2.12:
from typing import Literal, Optional
import anthropic
from pydantic import BaseModel, Field
class Lead(BaseModel):
name: str = Field(min_length=2)
email: str
plan: Literal["Starter", "Team", "Enterprise"]
seats: int = Field(ge=1, le=500)
phone: Optional[str] = None
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
response = client.messages.parse(
model="claude-haiku-5-5",
max_tokens=512,
messages=[{
"role": "user",
"content": "John Smith (john@example.com) wants the Enterprise plan for 40 seats.",
}],
output_format=Lead,
)
print(response.parsed_output)Run on October 30, 2026 with base_url="https://api.laozhang.ai" passed to the client, it returned name="John Smith", plan="Enterprise", seats=40 and phone=None, using 489 input tokens.
Disclosure: laozhang.ai is our own service. Its prices are platform pricing, not an independent comparison.
The part worth understanding is what happens to min_length, ge and le. The API doesn't accept those constraints, so the SDK removes them from the schema it sends and writes them into the field descriptions instead. anthropic.transform_schema(Lead) shows the result without making a call, and it also adds "additionalProperties": false to the object:
"name": {"type": "string", "title": "Name", "description": "{minLength: 2}"},
"seats": {"type": "integer", "title": "Seats", "description": "{maximum: 500, minimum: 1}"},
"phone": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Phone", "description": "{default: None}"}Claude therefore sees your limits only as a hint, and the SDK enforces them after the response arrives. When a second prompt said a customer had 0 seats, the API returned "seats": 0 and messages.parse() raised a pydantic ValidationError reading "Input should be greater than or equal to 1". Catch that exception and decide whether to retry, send the record to a person or store it as invalid, because the decoding step did nothing to prevent it.
TypeScript and Zod: zodOutputFormat()
In TypeScript, Anthropic's documentation wraps a Zod schema in zodOutputFormat() and passes it to client.messages.parse() as output_config.format; parse() validates the reply and returns a typed parsed_output:
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
const Contact = z.object({
name: z.string(),
email: z.string(),
plan_interest: z.string(),
demo_requested: z.boolean(),
});
const client = new Anthropic();
const response = await client.messages.parse({
model: "claude-haiku-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Extract the contact details from: ..." }],
output_config: { format: zodOutputFormat(Contact) },
});
console.log(response.parsed_output);If you pass a plain JSON schema through jsonSchemaOutputFormat() instead, the schema is transformed the same way but the response is not validated, so a rule like minimum goes unchecked unless you validate it yourself. The Java SDK takes a third approach and rejects unsupported constraints locally before sending.
JSON outputs or strict tool use: decide by who consumes the JSON
Use JSON outputs when the JSON is Claude's final answer and your code reads it: extraction, classification, a structured report. Use strict: true when Claude calls one of your functions and a wrong type would break it, such as "two" arriving where your booking code expects the integer 2. In an agent loop that needs valid tool calls and a structured final answer, use both; Anthropic's structured outputs documentation describes that combination as supported in a single request.
| JSON outputs | Strict tool use | |
|---|---|---|
| What is constrained | Claude's reply text | The tool name and its input |
| Where you set it | output_config.format on the request | "strict": true on each tool |
| Where the JSON appears | A text block, as a string | A tool_use block, input already an object |
Normal stop_reason | end_turn | tool_use |
| Schema rules | The subset in the next section | The same subset |
| Complexity cap | Counts toward the shared limits | Same, plus at most 20 strict tools per request |
A strict tool looks like any other tool with one extra key. This definition was sent with tool_choice forcing the tool, using the same email as the cURL example:
{
"name": "create_demo_booking",
"description": "Book a product demo for a lead.",
"strict": true,
"input_schema": {
"type": "object",
"properties": {
"email": {"type": "string", "format": "email"},
"plan": {"type": "string", "enum": ["Starter", "Team", "Enterprise"]},
"weekday": {"type": "string", "enum": ["Mon", "Tue", "Wed", "Thu", "Fri"]},
"hour_24": {"type": "integer"}
},
"required": ["email", "plan", "weekday", "hour_24"],
"additionalProperties": false
}
}Through LaoZhang API on October 30, 2026, Claude Haiku 5.5 stopped with stop_reason: "tool_use" and the input {"email": "john@example.com", "hour_24": 14, "plan": "Enterprise", "weekday": "Tue"}, turning "next Tuesday at 2pm" into an enum value and an integer your code can use directly. If you are moving from OpenAI, its text.format and function tools split the same way, as covered in Structured Outputs vs Function Calling: Choose the Right OpenAI API Contract.
Disclosure: laozhang.ai is our own service. Its prices are platform pricing, not an independent comparison.
JSON Schema rules: what returns 400 and what slips through
Both features accept the same subset of JSON Schema. Basic types, enum with primitive values, const, anyOf, allOf without $ref inside, local $ref and $defs, required and ten string formats work; recursion, numeric and length constraints, and open objects don't. Anthropic's documentation says an unsupported keyword returns a 400 with details, but through LaoZhang API on October 30, 2026 only some of them did, which is why the third column decides what you validate in code.
Disclosure: laozhang.ai is our own service. Its prices are platform pricing, not an independent comparison.

| Keyword | Anthropic documentation | Through LaoZhang API, October 30, 2026 | What to do |
|---|---|---|---|
additionalProperties: false | Required on every object | Omitting it still returned 200 and the right four fields | Always set it; SDK helpers add it for you |
minimum, maximum, multipleOf | Not supported, 400 | minimum: 1 returned the 400 shown below | Remove them and check ranges in code |
minLength, maxLength | Not supported, 400 | Accepted; a 12-character rule still produced {"code": "ABC"} | Check lengths in code |
Recursive $ref | Not supported | 400, circular reference | Flatten the tree or fix the depth |
enum, const | Primitive values only; capitalization not guaranteed | 5 of 5 replies matched the exact casing | Compare case-insensitively anyway |
String format | date-time, time, date, duration, email, hostname, uri, ipv4, ipv6, uuid | email worked in the strict tool above | Stick to the list; SDKs filter the rest out |
minItems | Only 0 or 1 | — | Check array sizes in code |
pattern | Simple regex; no backreferences, lookarounds or \b | — | Keep patterns simple or validate after |
| Optional properties | Allowed, at most 24 per request | An optional string came back as null | Treat optional as absent or null |
The two 400s came back as invalid_request_error with these messages, which you can match in logs:
output_config.format.schema: For 'integer' type, property 'minimum' is not supportedoutput_config.format.schema: Invalid schema: Circular reference detected in schema definitions: node -> node. Self-referencing or mutually-referencing definitions are not supported.Size limits explain why large schemas fail even when every keyword is legal. Across all strict schemas in one request, Anthropic allows 20 strict tools, 24 optional parameters and 16 parameters with union types, and a nullable field written as anyOf or ["string", "null"] counts as a union. Past those numbers, or past internal grammar limits, the API returns a 400 reading "Schema is too complex for compilation", with a 180-second compilation timeout as the last stop. Anthropic's suggested order is to mark only critical tools strict, make parameters required, flatten nested objects and then split the work into separate requests.
After a 200: refusals, max_tokens cuts and fields to recheck
A 200 with structured outputs means the request was valid, not that the JSON is complete or follows your business rules. Anthropic names two cases where the output may not match the schema, refusals and max_tokens cuts, and the October 30 tests turned up several replies that were valid JSON yet would still trip naive code:
| What happened | What comes back | Guard |
|---|---|---|
stop_reason: "refusal" | HTTP 200, billed, and text that may not match the schema | Don't parse it. A property that asks for step-by-step reasoning can trigger a reasoning_extraction refusal, so ask for a short explanation instead |
stop_reason: "max_tokens" | HTTP 200 with cut-off JSON; with max_tokens: 30, the text ended at {"ideas": ["REST vs GraphQL vs gRPC: How to Choose the | Retry with a higher max_tokens, sized for the longest array you expect |
| A rule outside the grammar | Valid JSON that breaks your rules, such as seats: 0 against ge=1 | Validate with Pydantic, Zod or a JSON Schema validator |
| Key order | Docs: schema order with required fields first. Through LaoZhang API, every reply had keys in alphabetical order | Read fields by name, never by position |
| Optional property | An optional phone typed as a plain string came back as null instead of being left out | Accept both missing and null |
| Enum casing | Docs: a value can differ only in case, such as "Conversation Topic 3" for "Conversation topic 3" | Compare case-insensitively and avoid values that differ only by case |
Disclosure: laozhang.ai is our own service. Its prices are platform pricing, not an independent comparison.
A small wrapper covers the first three rows for raw messages.create() calls and pairs well with any Pydantic model:
import json
def read_structured(response, model_cls):
if response.stop_reason == "refusal":
raise RuntimeError("Claude refused; the text may not match the schema")
if response.stop_reason == "max_tokens":
raise RuntimeError("Output was cut off; retry with a higher max_tokens")
text = next(block.text for block in response.content if block.type == "text")
data = json.loads(text) # fails fast if a proxy dropped the schema
return model_cls.model_validate(data) # enforces lengths, ranges and Literal valuesThe wrapper looks for the text block by type rather than taking content[0], because extended thinking can put thinking blocks first. The grammar applies only to Claude's final output, not to thinking, tool calls or tool results. A Claude Sonnet 5.5 request with thinking: {"type": "adaptive"} and output_config.effort set to low returned only a text block with valid JSON; at higher effort a thinking block can come first, which the wrapper already skips.
Prose instead of JSON, or "Extra inputs are not permitted"
A prose answer with HTTP 200 means the schema never reached the model; a 400 about extra inputs means something between you and Claude refused a field. Which one you see depends on the route. Anthropic's documentation says the direct API answers the deprecated top-level output_format with a 400 unless you add the beta header. Through LaoZhang API on October 30, 2026, the same legacy request returned 200 and a bulleted prose summary both with and without anthropic-beta: structured-outputs-2025-11-13, and it used 64 input tokens, exactly as many as the prompt with no schema at all, while the output_config.format version used 333.
Disclosure: laozhang.ai is our own service. Its prices are platform pricing, not an independent comparison.

Work through these checks in order when JSON doesn't come back:
- Move the schema to
output_config.formatand drop the beta header. Code copied from older tutorials often still sendsoutput_format, or callsclient.beta.messages.parse(..., betas=[...]). - Compare
usage.input_tokenswith the same prompt sent without a schema. If the number didn't rise, the schema was stripped somewhere on the way; the four-field schema here added 269 tokens. - Run
json.loads()on the text before anything else touches it, and treat a parse failure on a 200 as a routing problem first. - If the error says
Extra inputs are not permittedor is aValidationException, the platform in the middle checks your body against its own list of fields, whether the rejected key isoutput_configitself or one inside it such aseffort. Check what that platform accepts; Bedrock's versions of these messages are listed in AWS Bedrock Claude 400 Error: Match the Exact Message to the Fix. - If Python raises
TypeError, you passedoutput_format={...}toclient.beta.messages.create()orcount_tokens(), which SDK v1.0 and later reject. Useoutput_configthere, or switch toclient.messages.parse().
Prefilling the reply with { to force JSON doesn't combine with structured outputs. On Claude Haiku 5.5 the request failed with "This model does not support assistant message prefill. The conversation must end with a user message."; which models reject prefill and what to use instead is covered in Claude "Does Not Support Assistant Message Prefill" 400 Fix.
Cost of the format prompt: 269 extra input tokens in one test
Structured outputs make each request slightly more expensive, because Claude receives an extra system prompt describing the format and you pay for those tokens. With the four-field contact schema on Claude Haiku 5.5, the same prompt used 333 input tokens with the schema and 64 without, so the schema and its injected instructions added 269 tokens. The cost per request is 269 × the input price per million tokens ÷ 1,000,000. At the list prices on Anthropic's Claude API pricing page as of October 30, 2026, a million requests carry an extra $26.90 on Claude Haiku 5.5 ($0.10 per million input tokens for prompts up to 100,000 tokens), $538 on Sonnet 5.5 ($2) and $1,076 on Opus 5.5 ($4).
The output side can offset part of that. In the same pair of requests, output fell from 91 tokens of prose to 46 tokens of JSON, and at Sonnet 5.5's $10 per million output tokens those 45 tokens are worth $0.00045 per call, against $0.000538 of extra input. That is one short extraction, so treat it as a sign that compact JSON can roughly pay for the format prompt rather than as a rule; bigger schemas add more input tokens. The Batch API halves both sides, and how Claude's rates compare with OpenAI's per task is in Claude API vs OpenAI API Pricing: When Each One Costs Less.
Two side effects matter more than the token count. Changing output_config.format invalidates the prompt cache for that conversation thread, so keep the schema fixed for the life of a cached conversation. And the first request with a new schema waits for the grammar to compile; Anthropic caches compiled grammars for 24 hours from last use and keeps the cache when you change only name or description fields. Through the gateway, a never-seen schema and two identical repeats took 4.23, 3.33 and 4.28 seconds, so network variation hid any compile delay; if first-call latency matters, measure it on your own route.
Claude structured output: other questions
Which Claude models support structured outputs?
As of October 30, 2026, Anthropic's structured outputs page lists Claude Opus 5.5, Sonnet 5.5 and Haiku 5.5, Opus 5 and Sonnet 5, Fable 5.1, Fable 5, Mythos 5.1, Mythos 5 and Mythos Preview, Opus 4.8, 4.7, 4.6 and 4.5, Sonnet 4.6 and 4.5, and Haiku 4.5 (claude-haiku-4-5-20251001). Claude 3 models and Opus 4.1 are not on the list, so plan to move off them before relying on output_config.format.
Does Claude structured output work on Amazon Bedrock and Google Cloud?
Yes. The same Anthropic page marks structured outputs as generally available on Amazon Bedrock, including InvokeModel, on Claude Platform on AWS, on Claude models in Google Cloud whether Anthropic or Google manages them, and on Microsoft Foundry. It is not available in Claude Managed Agents. Each cloud documents its own request details, so check the provider's structured outputs page for the model you deploy.
Can I stream structured output or send it through the Batch API?
Yes to both. As of October 30, 2026, Anthropic documents structured outputs as working with streaming, token counting and the Batch API with its 50% discount. Citations are the exception: enabling them together with output_config.format returns a 400, because citation blocks can't be interleaved with schema-bound JSON.
Is Claude Code's structured output the same feature?
No. In Claude Code and the Agent SDK you pass a JSON Schema to query() as outputFormat in TypeScript or output_format in Python, and the SDK validates the result, re-prompts on a mismatch and returns an error once its retry limit is reached; the details are in the Claude Agent SDK structured outputs guide.
Can I use structured outputs with zero data retention or HIPAA workloads?
Yes, with one condition on the schema. Under zero data retention, prompts and outputs aren't stored, but the JSON schema itself is cached for up to 24 hours since last use. Structured outputs are HIPAA eligible, provided no protected health information appears in property names, enum or const values, or pattern expressions, according to Anthropic's data handling notes as of October 30, 2026.
Sources3
External pages this guide links to, in the order they appear. Last updated Oct 30, 2026.
Sources3
External pages this guide links to, in the order they appear. Last updated Oct 30, 2026.





