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

- URL: https://blog.laozhang.ai/en/posts/claude-structured-output
- Published: 2026-10-30
- Updated: 2026-10-30
- Author: LaoZhang AI Team (https://blog.laozhang.ai/en/about)
- Topic: API Guides
- Tags: Claude API, Structured Outputs, JSON Schema, Strict Tool Use, Pydantic, Anthropic

---
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](https://blog.laozhang.ai/en/about#laozhang). 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](https://blog.laozhang.ai/en/about#laozhang). 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](https://docs.laozhang.ai/faq/claude-code-aws-400-validation-error) 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](https://platform.claude.com/docs/en/build-with-claude/structured-outputs), 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](https://blog.laozhang.ai/en/about#laozhang). 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](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#using-both-features-together) 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:

```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](https://blog.laozhang.ai/en/posts/structured-outputs-vs-function-calling).

*Disclosure: [laozhang.ai is our own service](https://blog.laozhang.ai/en/about#laozhang). 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](https://blog.laozhang.ai/en/about#laozhang). 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](https://blog.laozhang.ai/posts/en/claude-structured-output/img/schema-keyword-support.webp)

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

```text
output_config.format.schema: For 'integer' type, property 'minimum' is not supported
```

```text
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 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](https://blog.laozhang.ai/en/about#laozhang). 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](https://blog.laozhang.ai/en/about#laozhang). 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](https://blog.laozhang.ai/posts/en/claude-structured-output/img/json-missing-triage.webp)

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](https://blog.laozhang.ai/en/posts/aws-claude-400-error).
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](https://blog.laozhang.ai/en/posts/claude-opus-prefill-error-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](https://platform.claude.com/docs/en/about-claude/pricing) 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](https://blog.laozhang.ai/en/posts/claude-api-vs-openai-api-pricing).

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](https://code.claude.com/docs/en/agent-sdk/structured-outputs).

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

## Sources

External pages this guide links to, in the order they appear. Last updated 2026-10-30.

- [structured outputs documentation](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) (platform.claude.com)
- [Claude API pricing page](https://platform.claude.com/docs/en/about-claude/pricing) (platform.claude.com)
- [Claude Agent SDK structured outputs guide](https://code.claude.com/docs/en/agent-sdk/structured-outputs) (code.claude.com)
