AI engineering

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.

By EpiFive Editorial TeamReviewed/updated 9 min readHow we check articles
On this page

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

FeatureParameterWhat it controls
JSON outputsoutput_config.formatThe format of Claude's own response
Strict tool usestrict: true on a toolThe 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.

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 ([email protected]) 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:

name='John Smith' email='[email protected]' 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.

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 ([email protected]) 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.

SupportedNot supported
Types: object, array, string, integer, number, boolean, nullRecursive schemas
enum of strings, numbers, booleans or nulls, and constComplex 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: falseminLength, maxLength
String formats such as date-time, date, email, uri, uuidArray constraints beyond minItems of 0 or 1
minItems of 0 or 1additionalProperties 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:

LimitValue
Strict tools per request20
Optional parameters across strict tools and JSON schemas24
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.

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

SituationWhat you seeWhat to do
Refusalstop_reason: "refusal", HTTP 200, billed for generated tokensDo not parse. The output may not match the schema. Handle it as a declined request
Token limit reachedstop_reason: "max_tokens"The JSON may be incomplete. Retry with a higher max_tokens
Unsupported schema feature400 error with detailsRemove the feature from the schema
Schema too complex400 error "Schema is too complex for compilation"Simplify the schema. Compilation also has a 180-second timeout
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 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

Questions people ask

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.

Claude APIAnthropicJSON SchemaPythonPydantic
  1. Claude Message Batches API: Cut Bulk Job Costs in Python

    Run large asynchronous Claude jobs at 50 percent of standard API prices with the Message Batches API: Python create, poll and results code, limits and fixes.

    AI engineering9 min read
  2. LLM Model Routing: Send Easy Requests to a Cheaper Model

    Build a simple Python router that sends each request to a small or large Claude model, then measure cost and quality against an eval set before you ship it.

    AI engineering12 min read
  3. Getting Started with LLM Evals: A Small-Team Setup with Code

    Build your first LLM eval in an afternoon: write 30 test cases in a JSONL file, grade them with simple code, add an LLM grader for tone, and track the score between prompt changes.

    AI engineering9 min read
  4. Anthropic Prompt Caching: Cut Cost and Latency on Repeated Context

    Use Claude prompt caching to cut input cost on repeated prompts: cache_control placement, 5-minute and 1-hour TTLs, pricing maths, minimum lengths and fixes.

    AI engineering9 min read
  5. Claude Tool Use in Python: Build a Simple Agent Loop Step by Step

    Build a tool-calling agent loop with the Claude Messages API in Python: define tools, run tool_use blocks, return tool_result blocks and stop safely.

    AI engineering11 min read
  6. LLM API Timeouts, Retries and Rate Limits: A Python Reliability Guide

    Make Claude and OpenAI API calls in Python resilient: set timeouts, retry only the right errors, honor retry-after on 429, and tell spend limits from rate limits.

    AI engineering10 min read