Skip to content
,

Structured Outputs Tutorial (2026): JSON Schema with Claude, OpenAI and Gemini in Python (Pydantic)

8–12 minutes
Curious woman in a teal shirt in a night office holding a JSON card, hook 3 APIs one JSON schema

Getting clean JSON out of an LLM used to mean begging in the prompt and hoping. Today Claude, OpenAI and Gemini all offer structured outputs: you pass a JSON schema, and the model’s reply is forced to match it.

This tutorial uses one Pydantic model and shows the exact Python code for all three APIs. You also get each provider’s limits, a side-by-side comparison table, and fixes for the errors people hit most. Checked against the official docs on November 9, 2026.

TLDR

  • Claude: client.messages.parse(..., output_format=Model), read parsed_output. The raw parameter is output_config.format, no beta header needed.
  • OpenAI: client.responses.parse(..., text_format=Model), read output_parsed.
  • Gemini: pass Model.model_json_schema() as the schema, then validate with Pydantic yourself.
  • Biggest gotchas: OpenAI needs every field required, Claude limits optional and union fields, Gemini ignores length rules.
  • Always validate: refusals and token cut-offs can still break the schema on every provider.

What Structured Outputs Are (and Why JSON Mode Is Not Enough)

Structured outputs use constrained decoding. The provider turns your schema into a grammar, and the model can only produce tokens that fit it. The result parses every time, with the fields and types you asked for.

That is different from older “JSON mode”, which only promises valid JSON.

  • JSON mode: valid JSON, but fields can be missing, renamed or the wrong type.
  • Structured outputs: valid JSON and it follows your schema.
  • Tool or function calling: use this when the model should call your code. Use structured outputs when you want the reply itself as data.

OpenAI’s docs recommend structured outputs over JSON mode whenever possible. Anthropic also has a sister feature called strict tool use (strict: true) that applies the same guarantee to tool inputs.

Claude vs OpenAI vs Gemini: Structured Outputs Compared

Feature Claude (Anthropic) OpenAI Gemini (Google)
Raw parameter output_config.format with type: "json_schema" text.format (Responses) or response_format (Chat Completions) response_format with a schema (Interactions API)
Python Pydantic helper messages.parse returns parsed_output responses.parse returns output_parsed No parse helper; use model_validate_json
Models All current models GPT-4o and later Gemini 3 and 4 series
All fields required? No (optional allowed, max 24) Yes (use nullable for optional) No
additionalProperties: false Required (SDK adds it) Required Optional
Recursive schemas Not supported Supported Supported
Min/max numbers Not enforced (SDK moves to description) Supported Supported
String length rules Not enforced Supported Ignored
Streaming Yes Yes Yes (partial JSON chunks)
Published size limits 20 strict tools, 24 optional, 16 union fields 5,000 properties, 10 levels deep None published
Quick answer: OpenAI supports the widest range of schema rules, Claude has the smoothest Pydantic helper with automatic schema clean-up, and Gemini is the most relaxed but leaves more validation to you.

Setup and the Shared Pydantic Model

We will extract invoice details from messy text. The same Pydantic model works for all three providers.

  1. Create a virtual environment with Python 3.10 or newer.
  2. Install the SDKs: pip install anthropic openai google-genai pydantic
  3. Set your keys as environment variables: ANTHROPIC_API_KEY, OPENAI_API_KEY and GEMINI_API_KEY.
  4. Save the model below in schema.py and import it in each example.
  5. Run each script and compare the parsed objects.
from typing import Optional
from pydantic import BaseModel, Field

class LineItem(BaseModel):
    description: str
    quantity: int
    unit_price: float

class Invoice(BaseModel):
    vendor: str = Field(description="Company that sent the invoice")
    invoice_number: str
    issue_date: str = Field(description="Date in YYYY-MM-DD format")
    currency: str = Field(description="Three-letter code like USD or EUR")
    items: list[LineItem]
    total: float
    po_number: Optional[str]  # nullable, still required

TEXT = """Invoice INV-2291 from Northwind Supplies, dated 3 Nov 2026.
2 x USB-C dock at $89.50 each, 1 x 27-inch monitor at $249.00.
Total due: $428.00 USD. No purchase order number."""

Tip: Note po_number: Optional[str] with no default. That makes it “required but can be null”, which is the pattern OpenAI needs and which works on Claude and Gemini too.

New to these SDKs? My Claude API, OpenAI API and Gemini API Python tutorials cover keys and basic calls.

Claude Structured Outputs in Python

Anthropic’s structured outputs work on all current Claude models and all cloud platforms, with no beta header. The Python SDK’s messages.parse helper takes your Pydantic class directly.

from anthropic import Anthropic
from schema import Invoice, TEXT

client = Anthropic()

response = client.messages.parse(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": f"Extract the invoice details:\n{TEXT}"}],
    output_format=Invoice,
)

invoice = response.parsed_output
print(invoice.total, invoice.items[0].description)

What the Claude SDK does for you

  • Cleans the schema: removes rules Claude cannot enforce, such as minimum or maxLength.
  • Keeps the hint: adds each removed rule to the field description, so the model still sees it.
  • Locks objects: adds additionalProperties: false everywhere.
  • Validates: checks the reply against your original model and returns it in parsed_output.

Without the helper, send the schema yourself in output_config={"format": {"type": "json_schema", "schema": {...}}} and parse the text block with json.loads. The SDK’s transform_schema() turns a Pydantic model into a compatible schema.

Warning: the old output_format request parameter is deprecated. It now returns a 400 error unless you send the structured-outputs-2025-11-13 beta header, so move raw calls to output_config.

Claude compatibility notes

  • Works with: streaming, extended thinking, batch processing (50% off) and strict tool use in the same request.
  • Does not work with: citations (400 error) or message prefilling.
  • First call is slower: the grammar compiles once, then is cached for 24 hours from last use.
  • Prompt cache: changing the format resets the prompt cache for that thread.

OpenAI Structured Outputs in Python

OpenAI supports structured outputs on GPT-4o and every later model. Its docs suggest gpt-6-astra for new projects. With the Responses API, use responses.parse and text_format.

from openai import OpenAI
from schema import Invoice, TEXT

client = OpenAI()

response = client.responses.parse(
    model="gpt-6-astra",
    input=[
        {"role": "system", "content": "Extract the invoice details."},
        {"role": "user", "content": TEXT},
    ],
    text_format=Invoice,
)

invoice = response.output_parsed
print(invoice.total, invoice.po_number)

On the older Chat Completions API, the same idea uses client.chat.completions.parse(..., response_format=Invoice) and choices[0].message.parsed.

OpenAI schema rules to remember

  • Root is an object: the top level cannot be an anyOf.
  • Every field is required: show optional values as nullable, like Optional[str] with no default.
  • No extra keys: additionalProperties: false on every object (the SDK handles this).
  • Not supported: allOf, not, if/then/else, dependentRequired and dependentSchemas.
  • Refusals: a safety refusal comes back as a refusal, not as your object. Check for it before using the data.

Gemini Structured Outputs in Python

Google’s current docs (updated October 21, 2026) show structured output through the Interactions API in the google-genai SDK. You pass a plain JSON Schema, which Pydantic generates for you.

from google import genai
from schema import Invoice, TEXT

client = genai.Client()

interaction = client.interactions.create(
    model="gemini-3.8-flash",
    input=f"Extract the invoice details:\n{TEXT}",
    response_format={
        "type": "text",
        "mime_type": "application/json",
        "schema": Invoice.model_json_schema(),
    },
)

invoice = Invoice.model_validate_json(interaction.output_text)
print(invoice.vendor, invoice.total)

If you use the older generate_content method, the same schema goes in the config as response_mime_type="application/json" plus response_json_schema.

Gemini notes

  • Supported keywords: types, enum, format (date-time, date, time), minimum/maximum, items, prefixItems, minItems/maxItems, plus anyOf and recursive $ref.
  • Silently ignored: minLength, maxLength, minProperties, maxProperties, uniqueItems and contains. Check these in your own code.
  • Tools: Gemini 3 series models can combine structured output with Google Search, URL Context, Code Execution, File Search and function calling (preview).
  • Streaming: with stream=True you get partial JSON text chunks that join into the final object.

Schema Limits per Provider

Every provider has a ceiling on schema size and complexity. Hitting it gives a 400 error, not a bad answer.

Limit Claude OpenAI Gemini
Object properties Not published 5,000 total Not published
Nesting depth Not published 10 levels Not published
Optional fields 24 across all strict schemas None (all required) Not published
Union (anyOf) fields 16 Not published Many alternatives can fail
Enum values Not published 1,000 across the schema Many values can fail
Name and enum text budget Not published 120,000 characters Very long names can fail
Strict tools per request 20 n/a n/a
Compile timeout 180 seconds Not published Not published

Note: OpenAI’s numeric limits come from a developer comparison that cites OpenAI’s docs (October 2026). Anthropic and Google publish their own figures and error causes directly.

One Function for All Three Providers

If you switch providers or want a fallback, wrap the three calls behind one function. Pydantic gives you the same object no matter which model answered.

from anthropic import Anthropic
from openai import OpenAI
from google import genai
from pydantic import ValidationError
from schema import Invoice, TEXT

claude = Anthropic()
openai_client = OpenAI()
gemini = genai.Client()

def extract(provider: str, text: str, model_cls):
    if provider == "claude":
        r = claude.messages.parse(
            model="claude-opus-5-5", max_tokens=1024,
            messages=[{"role": "user", "content": text}],
            output_format=model_cls,
        )
        return r.parsed_output
    if provider == "openai":
        r = openai_client.responses.parse(
            model="gpt-6-astra", input=text, text_format=model_cls,
        )
        return r.output_parsed
    if provider == "gemini":
        r = gemini.interactions.create(
            model="gemini-3.8-flash", input=text,
            response_format={"type": "text", "mime_type": "application/json",
                             "schema": model_cls.model_json_schema()},
        )
        return model_cls.model_validate_json(r.output_text)
    raise ValueError(provider)

for p in ["claude", "openai", "gemini"]:
    try:
        print(p, extract(p, TEXT, Invoice))
    except ValidationError as e:
        print(p, "did not match the schema:", e)

Want to compare what each call costs? See my AI API pricing comparison.

Schema Design Tips That Save Pain

  • Describe every field: Field(description=...) is the model’s main hint about what goes where.
  • Use enums for categories: they stop the model from inventing labels. On Claude, compare enum values case-insensitively, as its docs warn casing can shift.
  • Keep it flat: fewer nested levels and optional fields compile faster and fail less.
  • Avoid “reasoning” fields on Claude: properties that ask for step-by-step thinking can trigger a refusal. Ask for a short explanation instead.
  • Put order-sensitive fields first: Claude outputs required fields before optional ones.
  • Validate business rules yourself: a schema-valid total can still be wrong. Check sums and dates in code.

Troubleshooting: Common Problems and Fixes

  • Claude 400 “Schema is too complex for compilation”: cut optional fields below 24, union fields below 16, and mark fewer tools as strict.
  • Claude 400 on output_format: you are sending the deprecated raw parameter. Switch to output_config.format.
  • Claude 400 with citations: citations cannot be combined with JSON outputs. Turn one off.
  • OpenAI “all fields must be required”: remove Pydantic defaults and use Optional[...] for fields that may be empty.
  • OpenAI output_parsed is None: the model refused or stopped early. Check the refusal and the response status.
  • Gemini 400 INVALID_ARGUMENT: shorten names, cut enum values, reduce nested maxItems and narrow numeric ranges.
  • Gemini ignores my length limit: minLength and maxLength are stripped. Validate length after parsing.
  • Cut-off JSON on any provider: the reply hit the token limit. Raise max_tokens and retry.
  • Rate limit errors: see my 429 and 529 error fix guide.

Frequently Asked Questions

Does Claude support structured outputs without a beta header?

Yes. Claude’s structured outputs use output_config.format and work on all current models with no beta header. Only the deprecated output_format parameter needs the old beta header.

What is the difference between JSON mode and structured outputs?

JSON mode only guarantees valid JSON. Structured outputs also guarantee the reply matches your schema’s fields and types.

Can I use the same Pydantic model for Claude, OpenAI and Gemini?

Yes, if you keep it simple. Make optional fields nullable with no default, avoid recursion for Claude, and validate length rules yourself for Gemini.

Which provider is best for strict JSON?

OpenAI enforces the widest set of schema rules, including number ranges and string patterns. Claude is very reliable with simpler schemas, and Gemini is flexible but ignores some rules.

Do structured outputs cost extra?

No separate fee. Claude adds a small system prompt that uses extra input tokens, and the first call is slower while the grammar compiles.

Can structured outputs still fail?

Yes. A refusal or hitting the token limit can return output that does not match the schema, so always check the stop reason and validate.

Do structured outputs work with streaming?

Yes, on all three. You receive partial JSON as it is generated and parse it once the stream ends.

Sources

Free weekly email

Get new AI guides every week

One short weekly email with our newest local AI, ComfyUI and AI tool guides. No spam, no pop-ups, just the good stuff.

Free. Unsubscribe anytime in one click. We only use your email to send this newsletter. See our privacy policy.

Curious woman in a teal shirt in a night office holding a JSON card, hook 3 APIs one JSON schema