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
anthropicandpydanticpackages installed. - An API key available as the
ANTHROPIC_API_KEYenvironment 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-20250929andclaude-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:
| 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.
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=Trueresponse.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.
| 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.
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.
| 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 |
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_extractionrefusal. 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_formaton a raw request. Switch tooutput_config.format. TypeErroronoutput_format. In SDK v1.0 and later,output_format={...}is not accepted onclient.beta.messages.create()orcount_tokens(). Useoutput_config.- 400 on schema. Look for
minimum,maxLength, recursive references or an object withoutadditionalProperties: false. json.JSONDecodeError. Checkstop_reason. Amax_tokensstop produces truncated JSON.- Slow first call, fast later calls. This is grammar compilation and caching, not a fault.
Checklist
- Use
output_config.format(ormessages.parse), not the deprecatedoutput_formatheader route. - Set
additionalPropertiestofalseon every object. - Remove numeric and string-length constraints from raw schemas, or validate them in code.
- Check
stop_reasonforrefusalandmax_tokensbefore 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
Comments
Questions and tips are welcome. Comments are checked by the EpiFive team before they appear; please do not share passwords, phone numbers or other private details.