# Claude Structured Outputs: Get Valid JSON from the API in Python

> Use output_config with a JSON schema, or messages.parse with Pydantic, to get schema-valid JSON from Claude in Python, plus the schema limits and failure cases to handle.

Source: https://www.epifive.com/blog/claude-structured-outputs-json-schema-python · Published 2026-10-25 · EpiFive

## Key takeaways

- Set output_config.format with type json_schema to make Claude return JSON that matches your schema, with no beta header needed.
- In Python, client.messages.parse with a Pydantic model returns a validated object in response.parsed_output.
- Object schemas must set additionalProperties to false, and numeric and string-length constraints are not supported.
- Always check stop_reason, because a refusal or max_tokens stop can produce output that does not match the schema.
- The first request with a new schema is slower while the grammar compiles, and compiled grammars are cached for 24 hours from last use.

To get schema-valid JSON from Claude in Python, pass a JSON schema in `output_config.format` with `type: "json_schema"`, or pass a Pydantic model as `output_format` to `client.messages.parse()` and read the result from `response.parsed_output`. The API constrains generation to your schema, so you no longer need to retry on malformed JSON or strip Markdown fences from the reply.

## Who this is for and what you will have

This guide is for developers who use Claude for extraction or classification tasks, such as turning an email into a record, and need output that code can consume without defensive parsing. By the end you will have a working extraction script in two styles (Pydantic and raw schema), an understanding of which schema features are allowed, and error handling for the cases where output can still be invalid.

## Prerequisites

- Python with the `anthropic` and `pydantic` packages installed.
- An API key available as the `ANTHROPIC_API_KEY` environment variable.
- A model that supports structured outputs. The documentation lists, among others, `claude-opus-5-5`, `claude-sonnet-5-5`, `claude-haiku-5-5`, `claude-sonnet-4-5-20250929` and `claude-haiku-4-5-20251001`. Check the current list before you pick a model.

```bash
pip install anthropic pydantic
export ANTHROPIC_API_KEY="your-key-here"
```

Structured outputs are generally available on the Claude API, Amazon Bedrock, Google Cloud and Microsoft Foundry according to the documentation, so the same approach is not limited to one platform. Confirm the details for your platform on the official page.

## How structured outputs work

You describe the shape of the answer as a JSON schema. Claude's output is constrained to that schema, and the response arrives as normal text in a `text` content block that contains JSON. Claude receives an extra system prompt describing the output format, so expect a small increase in input tokens.

There are two related features, and you can combine them in one request:

| Feature | Parameter | What it controls |
| --- | --- | --- |
| JSON outputs | `output_config.format` | The format of Claude's own response |
| Strict tool use | `strict: true` on a tool | The names and inputs of tool calls |

The older `output_format` parameter is deprecated. If you still use it you must send the beta header `structured-outputs-2025-11-13`, otherwise the API returns a 400 error. In Python SDK v1.0 and later, passing `output_format={...}` to `client.beta.messages.create()` or `count_tokens()` raises a `TypeError`, so use `output_config` there.

## Option 1: Pydantic with messages.parse

This is the approach the documentation recommends. Define a model, pass it as `output_format`, and the SDK transforms the schema, sends it as `output_config.format`, validates the response and returns the parsed model.

```python
from anthropic import Anthropic
from pydantic import BaseModel


class ContactInfo(BaseModel):
    name: str
    email: str
    plan_interest: str
    demo_requested: bool


client = Anthropic()

response = client.messages.parse(
    model="claude-opus-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_format=ContactInfo,
)

print(response.parsed_output)
```

The output looks like this:

```text
name='John Smith' email='john@example.com' plan_interest='Enterprise' demo_requested=True
```

`response.parsed_output` is a `ContactInfo` instance, so you can use attributes such as `response.parsed_output.email` directly.

## Option 2: Raw JSON schema with messages.create

If you do not use Pydantic, or you keep schemas in JSON files, send the schema yourself. The response text is a JSON string that you load.

```python
import json

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-opus-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,
            },
        }
    },
)

text = next(block.text for block in response.content if block.type == "text")
contact = json.loads(text)
print(contact)
```

Note `additionalProperties: False` on the object. The documentation states that for objects it must be `false`, and any other value is unsupported. If you want the SDK's schema transformation without sending a request, the `anthropic` package provides `transform_schema()`, which accepts a dict or a Pydantic model.

## What the schema can and cannot contain

The supported subset of JSON Schema is narrower than the full specification. Using an unsupported feature returns a 400 error with details.

| Supported | Not supported |
| --- | --- |
| Types: object, array, string, integer, number, boolean, null | Recursive schemas |
| `enum` of strings, numbers, booleans or nulls, and `const` | Complex types inside enums |
| `anyOf` and `allOf` (but not `allOf` with `$ref`) | External `$ref` |
| `$ref`, `$def` and `definitions` (internal only) | `minimum`, `maximum`, `multipleOf` |
| `required` and `additionalProperties: false` | `minLength`, `maxLength` |
| String formats such as `date-time`, `date`, `email`, `uri`, `uuid` | Array constraints beyond `minItems` of 0 or 1 |
| `minItems` of 0 or 1 | `additionalProperties` set to anything but `false` |

Regular expression `pattern` values support anchors, simple quantifiers, character classes and groups. They do not support backreferences, lookahead or lookbehind, or `\b` and `\B`.

When you use the Pydantic or similar SDK helpers, unsupported constraints such as `minimum` are removed from the schema sent to the API and moved into the property description. The helper still enforces the original constraints when it validates the response locally, so a Pydantic `Field(ge=0)` is checked on your side even though the model is only told about it in text.

### Complexity limits

Even a schema that uses only supported features can be rejected. The documented limits are:

| Limit | Value |
| --- | --- |
| Strict tools per request | 20 |
| Optional parameters across strict tools and JSON schemas | 24 |
| Parameters with union types (`anyOf` or type arrays) | 16 |

Internal grammar-size limits also apply even if you stay within those numbers. To reduce complexity, make parameters required where you can, flatten nested structures, and mark only critical tools as strict.

## Combine JSON outputs with strict tool use

A request can use both features. JSON outputs control what Claude says, and strict tools control how it calls your functions. Claude may reply with a `tool_use` block or with JSON in a `text` block, so your code must handle both.

```python
response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": "Help me plan a trip to Paris departing May 15, 2026",
        }
    ],
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "summary": {"type": "string"},
                    "next_steps": {"type": "array", "items": {"type": "string"}},
                },
                "required": ["summary", "next_steps"],
                "additionalProperties": False,
            },
        }
    },
    tools=[
        {
            "name": "search_flights",
            "strict": True,
            "input_schema": {
                "type": "object",
                "properties": {
                    "destination": {"type": "string"},
                    "date": {"type": "string", "format": "date"},
                },
                "required": ["destination", "date"],
                "additionalProperties": False,
            },
        }
    ],
)
```

If you run an agent loop, see [Claude Tool Use in Python: Build an Agent Loop](https://www.epifive.com/blog/claude-tool-use-python-agent-loop) for how tool results are returned to the model.

## Handle the cases where output can still be invalid

Constrained generation removes most parsing failures, but not all. Check `stop_reason` before you trust the content.

| Situation | What you see | What to do |
| --- | --- | --- |
| Refusal | `stop_reason: "refusal"`, HTTP 200, billed for generated tokens | Do not parse. The output may not match the schema. Handle it as a declined request |
| Token limit reached | `stop_reason: "max_tokens"` | The JSON may be incomplete. Retry with a higher `max_tokens` |
| Unsupported schema feature | 400 error with details | Remove the feature from the schema |
| Schema too complex | 400 error "Schema is too complex for compilation" | Simplify the schema. Compilation also has a 180-second timeout |

```python
if response.stop_reason in ("refusal", "max_tokens"):
    raise RuntimeError(f"Unusable structured output: {response.stop_reason}")
```

Two further details matter for reliable code:

- **Enum casing.** Enum values may differ from your schema only in capitalization, typically the first letter of a word after a space. Compare enum values case-insensitively, and avoid enum values that differ only by case.
- **Reasoning properties.** A schema property that asks for the model's reasoning may trigger a `reasoning_extraction` refusal. Ask for a short explanation field instead.

Property order in the output is also fixed: required properties come first, then optional ones, each in schema order.

## Latency, caching and cost

The first request with a new schema is slower because the grammar must compile. Compiled grammars are cached for 24 hours from last use. Changing the structure of the schema invalidates that cache, and so does changing the set of tools when you combine structured outputs with tool use. Changing only a `name` or `description` does not.

Changing `output_config.format` also invalidates the prompt cache for that conversation thread, so keep the schema stable across a conversation if you rely on prompt caching. To estimate the extra input tokens from the format prompt, count them with your real request before you budget. [Claude API Pricing: Estimate Token Costs Before You Build](https://www.epifive.com/blog/claude-api-pricing-token-cost-estimate) shows how to turn token counts into a cost.

## Data handling

Structured outputs are eligible for zero data retention, with the exception of covered models as listed in the documentation. The JSON schema itself is cached for up to 24 hours since last use. The documentation notes that structured outputs are HIPAA eligible, but that protected health information must not appear in schema property names, `enum` or `const` values, or `pattern` expressions, because cached schemas do not receive the same protections as message content. Keep field names generic and put sensitive values only in the message content.

## Troubleshooting

- **400 error mentioning the beta header.** You are using the deprecated `output_format` on a raw request. Switch to `output_config.format`.
- **`TypeError` on `output_format`.** In SDK v1.0 and later, `output_format={...}` is not accepted on `client.beta.messages.create()` or `count_tokens()`. Use `output_config`.
- **400 on schema.** Look for `minimum`, `maxLength`, recursive references or an object without `additionalProperties: false`.
- **`json.JSONDecodeError`.** Check `stop_reason`. A `max_tokens` stop produces truncated JSON.
- **Slow first call, fast later calls.** This is grammar compilation and caching, not a fault.

## Checklist

- Use `output_config.format` (or `messages.parse`), not the deprecated `output_format` header route.
- Set `additionalProperties` to `false` on every object.
- Remove numeric and string-length constraints from raw schemas, or validate them in code.
- Check `stop_reason` for `refusal` and `max_tokens` before parsing.
- Keep the schema stable to benefit from grammar and prompt caching.
- Keep protected data out of property names, enums and patterns.

## References

- Structured outputs, Claude API documentation: https://platform.claude.com/docs/en/build-with-claude/structured-outputs

## Frequently asked questions

### Do I need a beta header to use Claude structured outputs?

Not with the current output_config.format parameter. The older output_format parameter is deprecated and still needs the beta header structured-outputs-2025-11-13, and without it the API returns a 400 error.

### Is the JSON guaranteed to match my schema every time?

Almost always, but two cases can break it. A refusal returns stop_reason "refusal" and the output may not match the schema, and a response cut off at max_tokens can be incomplete. Check stop_reason before you parse.

### Why does the API return "Schema is too complex for compilation"?

Your schema exceeds the grammar compilation limits. Mark only critical tools as strict, make parameters required instead of optional, flatten nested objects, or split the work across several requests.

### Can I use minimum, maxLength or other constraints in the schema?

Not in the schema sent to the API, which rejects numerical and string length constraints. The Python SDK helpers move them into descriptions and still enforce the original constraints when they validate the response locally.

