Skip to main content

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.

LaoZhang AI TeamPublished12 min read
On this page
Claude structured output: a JSON schema in output_config.format returns a JSON string in the text block, followed by a stop_reason check and local validation with Pydantic or Zod

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:

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

json
{"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:

python
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:

json
"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:

typescript
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 outputsStrict tool use
What is constrainedClaude's reply textThe tool name and its input
Where you set itoutput_config.format on the request"strict": true on each tool
Where the JSON appearsA text block, as a stringA tool_use block, input already an object
Normal stop_reasonend_turntool_use
Schema rulesThe subset in the next sectionThe same subset
Complexity capCounts toward the shared limitsSame, 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:

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

JSON Schema keywords sorted into three groups: minimum, maximum, multipleOf, recursive ref and oversized schemas return 400; minLength, maxLength, minItems, pattern and a missing additionalProperties need checks in code; enum, const, anyOf, allOf, local ref and string formats are enforced

KeywordAnthropic documentationThrough LaoZhang API, October 30, 2026What to do
additionalProperties: falseRequired on every objectOmitting it still returned 200 and the right four fieldsAlways set it; SDK helpers add it for you
minimum, maximum, multipleOfNot supported, 400minimum: 1 returned the 400 shown belowRemove them and check ranges in code
minLength, maxLengthNot supported, 400Accepted; a 12-character rule still produced {"code": "ABC"}Check lengths in code
Recursive $refNot supported400, circular referenceFlatten the tree or fix the depth
enum, constPrimitive values only; capitalization not guaranteed5 of 5 replies matched the exact casingCompare case-insensitively anyway
String formatdate-time, time, date, duration, email, hostname, uri, ipv4, ipv6, uuidemail worked in the strict tool aboveStick to the list; SDKs filter the rest out
minItemsOnly 0 or 1—Check array sizes in code
patternSimple regex; no backreferences, lookarounds or \b—Keep patterns simple or validate after
Optional propertiesAllowed, at most 24 per requestAn optional string came back as nullTreat 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 supported
output_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 happenedWhat comes backGuard
stop_reason: "refusal"HTTP 200, billed, and text that may not match the schemaDon'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 theRetry with a higher max_tokens, sized for the longest array you expect
A rule outside the grammarValid JSON that breaks your rules, such as seats: 0 against ge=1Validate with Pydantic, Zod or a JSON Schema validator
Key orderDocs: schema order with required fields first. Through LaoZhang API, every reply had keys in alphabetical orderRead fields by name, never by position
Optional propertyAn optional phone typed as a plain string came back as null instead of being left outAccept both missing and null
Enum casingDocs: 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:

python
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 values

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

Troubleshooting chart for missing JSON: a prose answer with HTTP 200 means the schema never arrived, with 64 instead of 333 input tokens; a 400 saying extra inputs are not permitted means a platform in between rejected a field, and a Python TypeError means old output_format code that should switch to output_config or messages.parse

Work through these checks in order when JSON doesn't come back:

  1. Move the schema to output_config.format and drop the beta header. Code copied from older tutorials often still sends output_format, or calls client.beta.messages.parse(..., betas=[...]).
  2. Compare usage.input_tokens with 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.
  3. Run json.loads() on the text before anything else touches it, and treat a parse failure on a 200 as a routing problem first.
  4. If the error says Extra inputs are not permitted or is a ValidationException, the platform in the middle checks your body against its own list of fields, whether the rejected key is output_config itself or one inside it such as effort. 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.
  5. If Python raises TypeError, you passed output_format={...} to client.beta.messages.create() or count_tokens(), which SDK v1.0 and later reject. Use output_config there, or switch to client.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.

  1. 1.structured outputs documentationplatform.claude.com/docs/en/build-with-claude/structured-outputs
  2. 2.Claude API pricing pageplatform.claude.com/docs/en/about-claude/pricing
  3. 3.Claude Agent SDK structured outputs guidecode.claude.com/docs/en/agent-sdk/structured-outputs
More in API Guides
A decision map comparing a direct typed response with application-executed tools and a final typed object
API Guides

Structured Outputs vs Function Calling: Choose the Right OpenAI API Contract

Use text.format when your application needs the model's final typed answer. Use function calling when the answer requires application data or an action. Combine them when a real tool result must become a schema-controlled object; then check both the schema and the business result.

11 min