Patterns
Ren Okabe5 min read2 views

Claude Structured Outputs in Python: Guaranteed JSON (2026)

A runnable Python guide to Claude structured output: the native output_config.format JSON path, Pydantic-driven schemas, strict tool use, a portable tool-use fallback, and the 400/truncation gotchas nobody covers.

Close-up of syntax-highlighted code on a dark screen, representing structured JSON output from a Claude model
Close-up of syntax-highlighted code on a dark screen, representing structured JSON output from a Claude model
On this page

Anthropic Python Getting a Claude model to return JSON you can json.loads() used to mean prompt-begging and defensive parsing. As of 2026, the Claude Developer Platform ships native structured outputs, so the format is guaranteed by the API rather than requested in a system prompt. This tutorial builds the reliable path in Python, then keeps the older tool-use fallback for the cases native support does not cover.

Quick answer (2026)

Claude structured output is a native API feature that constrains the model's decoding so the response is guaranteed to match a JSON Schema you supply. You get it two ways: output_config.format with type: "json_schema" for a top-level JSON response, or "strict": true on a tool definition for schema-valid tool calls. It is generally available on Claude 4.5 and newer models. Every object in your schema must set additionalProperties: false, and a handful of JSON Schema keywords (minLength, maximum, recursive $ref) are not supported, so those constraints still belong in your own validation code.

What structured outputs actually guarantee

The mechanism is constrained decoding. Before generation, your schema is compiled into a grammar, and at each step the sampler is masked to only tokens that keep the output valid against that grammar. The model physically cannot emit a response that breaks the schema shape. That is a stronger promise than "ask nicely and parse", and it removes the entire class of retry loops that existed to catch malformed JSON.

Two surfaces expose it:

  • JSON output format (output_config.format): the whole assistant message is one JSON document matching your schema. Use this when the model's job is extraction or classification and the answer is the data.
  • Strict tool use ("strict": true on a tool): the arguments Claude passes to a tool are guaranteed schema-valid. Use this inside agent loops where the structured value is a function call, not the final answer.

Both are generally available on Claude 4.5 and later on the Claude Developer Platform, per Anthropic's structured outputs documentation (2026). The design mirrors OpenAI's structured outputs (2026), so the schema discipline below ports across providers with minor field renames.

Assumptions

  • Python 3.10 or newer and the official anthropic Python SDK (pip install anthropic).
  • ANTHROPIC_API_KEY exported in your environment.
  • A current model id. The code uses a single MODEL constant set to claude-sonnet-5 so you can swap it without hunting through the file.
python
import os, json
from anthropic import Anthropic

client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
MODEL = "claude-sonnet-5"

A guaranteed JSON response

Define the schema as a plain dict. Note the two non-negotiables: additionalProperties: false on every object and a required array listing every property you expect.

python
INVOICE_SCHEMA = {
    "type": "object",
    "properties": {
        "vendor": {"type": "string"},
        "total": {"type": "number"},
        "currency": {"type": "string", "enum": ["USD", "EUR", "GBP"]},
        "line_items": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "description": {"type": "string"},
                    "amount": {"type": "number"},
                },
                "required": ["description", "amount"],
                "additionalProperties": False,
            },
        },
    },
    "required": ["vendor", "total", "currency", "line_items"],
    "additionalProperties": False,
}

def extract_invoice(text: str) -> dict:
    resp = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        messages=[{"role": "user", "content": f"Extract the invoice:\n\n{text}"}],
        output_config={"format": {"type": "json_schema", "schema": INVOICE_SCHEMA}},
    )
    if resp.stop_reason == "max_tokens":
        raise RuntimeError("response truncated; raise max_tokens and retry")
    return json.loads(resp.content[0].text)

The json.loads call cannot throw on a schema violation, because the API would not have returned invalid JSON. It can still throw on a truncated response, which is why the stop_reason guard sits before it. More on that below.

If your installed SDK build does not yet type the output_config keyword, pass it through untyped with extra_body={"output_config": {...}}; the wire format is identical.

Drive the schema from Pydantic

Hand-writing schemas gets tedious and drifts from your types. Generate the schema from a Pydantic model instead, then patch it to satisfy the constrained-decoding rules. Pydantic emits a draft schema that is close but not compliant (it omits additionalProperties: false and may include unsupported keywords), so a small normalizer earns its keep.

python
from pydantic import BaseModel

class LineItem(BaseModel):
    description: str
    amount: float

class Invoice(BaseModel):
    vendor: str
    total: float
    currency: str
    line_items: list[LineItem]

UNSUPPORTED = {"minLength", "maxLength", "minimum", "maximum",
               "multipleOf", "pattern", "format"}

def make_strict(node):
    if isinstance(node, dict):
        node = {k: make_strict(v) for k, v in node.items() if k not in UNSUPPORTED}
        if node.get("type") == "object":
            node["additionalProperties"] = False
            node["required"] = list(node.get("properties", {}).keys())
        return node
    if isinstance(node, list):
        return [make_strict(v) for v in node]
    return node

def extract(model_cls, text: str):
    schema = make_strict(model_cls.model_json_schema())
    resp = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        messages=[{"role": "user", "content": f"Extract:\n\n{text}"}],
        output_config={"format": {"type": "json_schema", "schema": schema}},
    )
    if resp.stop_reason == "max_tokens":
        raise RuntimeError("response truncated")
    return model_cls.model_validate_json(resp.content[0].text)

model_validate_json gives you a typed Invoice object with editor autocomplete downstream. Read the Pydantic JSON Schema reference (2026) if your models use $ref for nested types; make_strict flattens the common cases but not recursive definitions, which are unsupported anyway.

Strict tool use inside an agent loop

When the structured value is an action rather than the answer, put the schema on a tool and mark it strict. This is the shape you want in an orchestrator that returns a typed plan: the plan arrives as validated tool input.

python
BOOK_TOOL = {
    "name": "book_flight",
    "description": "Book a flight for the user.",
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "destination": {"type": "string"},
            "date": {"type": "string"},
        },
        "required": ["destination", "date"],
        "additionalProperties": False,
    },
}

resp = client.messages.create(
    model=MODEL,
    max_tokens=512,
    tools=[BOOK_TOOL],
    tool_choice={"type": "tool", "name": "book_flight"},
    messages=[{"role": "user", "content": "Fly me to Lisbon on 2026-09-14."}],
)
tool_use = next(b for b in resp.content if b.type == "tool_use")
args = tool_use.input          # already schema-valid, no parsing needed

Forcing tool_choice to the single tool guarantees the model calls it, and strict guarantees the arguments match. You never touch json.loads here; the SDK hands you a dict.

A fallback for models without native support

Self-hosted models, older Claude versions, and some gateway providers do not offer constrained decoding. Keep one portable path that works anywhere: force a tool call the classic way and defensively parse. It is less safe than native structured outputs, so treat it as a compatibility shim, not the default.

python
def extract_json_fallback(text: str, schema: dict) -> dict:
    resp = client.messages.create(
        model=MODEL,
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"Return ONLY JSON matching this schema:\n"
                       f"{json.dumps(schema)}\n\nInput:\n{text}",
        }],
    )
    raw = resp.content[0].text.strip()
    if raw.startswith("```"):                 # strip a ```json fence if present
        raw = raw.split("```", 2)[1]
        raw = raw[4:] if raw.lstrip().startswith("json") else raw
    start, end = raw.find("{"), raw.rfind("}")
    return json.loads(raw[start:end + 1])

This is the pattern to retire the moment your target model supports the native API.

Making it reliable in production

Native structured outputs remove the malformed-JSON failure mode, but three real ones remain.

Scroll to see more

FailureCauseFix
400 invalid_request_errorAn object without additionalProperties: false, or an unsupported keywordRun make_strict on every schema before sending
Silent constraint dropminLength, maximum, multipleOf and friends are ignored, not enforcedValidate value ranges in your own code after parsing
Truncated JSONmax_tokens hit mid-documentCheck stop_reason == "max_tokens" before parsing; raise max_tokens and retry

Two more that bite teams at scale:

  • First-call latency. Compiling a schema into a grammar and token masks is not free; the Hacker News thread on the launch (2025) flags it directly. Reuse the same schema object across calls so the compiled grammar stays warm, and expect the very first request against a new schema to be slower.
  • Rate limits still apply. Constrained decoding does not exempt you from 429 and 529 responses. Wrap these calls in the same retry-with-backoff wrapper you use for every other Claude request.

A verify step

Prove the whole path end to end before you ship it. This is the AgentNotebook habit: never trust a helper you have not watched succeed and fail.

python
if __name__ == "__main__":
    inv = extract(Invoice, "ACME Corp billed 2 units at 40 USD each, total 80.")
    assert inv.vendor and inv.total == 80 and inv.currency == "USD"
    assert len(inv.line_items) >= 1
    print("ok:", inv.model_dump())

Run it, then deliberately set max_tokens=8 and confirm your stop_reason guard raises instead of handing you half an object.

Limitations and open questions

  • Constrained decoding guarantees the shape, not the correctness. A schema-valid invoice can still have the wrong total. Structured outputs pair with, they do not replace, an evaluator that scores the content.
  • The supported JSON Schema subset is real: no minLength/maxLength, no numeric bounds, minItems only 0 or 1, no recursive schemas, no external $ref. Rich validation stays in application code.
  • Native support requires Claude 4.5 or newer on the Developer Platform. Bedrock and Vertex expose it for their own model lists; older or self-hosted models need the Step 4 fallback.
  • Grammar-compilation latency for very large or deeply nested schemas is not yet well documented. If a schema is enormous, benchmark the first call before you put it on a hot path.
R

Written by

Ren Okabe

Principal engineer writing AgentNotebook's production-reliability tutorials. Cares about the failure modes, not the happy path.

Frequently asked questions

Is Claude structured output the same as tool use?

They overlap but differ. Structured output via output_config.format returns the whole assistant message as one schema-valid JSON document, best for extraction or classification. Strict tool use puts the schema on a tool and guarantees the tool's arguments are valid, best inside agent loops where the structured value is an action. Both use the same constrained-decoding engine.

Which Claude models support structured outputs in 2026?

On the Claude Developer Platform, structured outputs are generally available on Claude 4.5 and newer models, including Sonnet 4.5, Haiku 4.5, and the Claude 5 family. Amazon Bedrock and Google Vertex AI expose it for their own supported model lists. Older or self-hosted models need the tool-use fallback shown in Step 4.

Do I still need Pydantic validation if the output is guaranteed?

The format is guaranteed, the values are not. Constrained decoding enforces the JSON shape, but it cannot enforce keywords it does not support (minLength, maximum, multipleOf) and it never checks whether the extracted numbers are correct. Keep Pydantic or your own checks for value-level rules and for parsing the response into a typed object.

Why am I getting a 400 error about additionalProperties?

Structured outputs require every object in the schema to set additionalProperties to false, and require the required array to list all properties. Pydantic's generated schema omits both by default. Run the response through a normalizer (the make_strict helper in Step 2) before sending, and drop unsupported keywords in the same pass.

What happens if the JSON response is truncated?

Constrained decoding guarantees valid tokens, not a complete document. If the response hits max_tokens mid-object, stop_reason is set to max_tokens and the text is incomplete, so json.loads will throw. Always check stop_reason before parsing, then retry with a higher max_tokens.

Can I use minLength or maximum in a structured-output schema?

No. As of 2026 the supported subset excludes minLength, maxLength, minimum, maximum, multipleOf, recursive schemas, and external $ref. minItems supports only 0 or 1. Enforce those constraints in your application code after parsing rather than in the schema.