Getting consistent, parseable JSON from large language models used to be a frustrating game of prompt engineering and regex parsing. Today, both OpenAI and Anthropic offer native structured output capabilities that guarantee valid JSON responses every time. This tutorial walks you through implementing structured output with both APIs, from basic usage to advanced patterns.

Why Structured Output Matters

When you integrate LLMs into applications, you rarely want free-form text. You need data your code can parse: product descriptions with specific fields, classification results with confidence scores, or extracted entities in a predictable format. Without structured output, you end up writing brittle parsing logic that breaks whenever the model decides to add a friendly preamble or format things differently.

Structured output solves this by constraining the model's response to follow a JSON schema you define. The model cannot produce invalid JSON or deviate from your schema — the output is guaranteed to be parseable.

Step 1: Basic JSON Mode with OpenAI

The simplest approach is OpenAI's JSON mode, which ensures the response is valid JSON (though not necessarily matching a specific schema).

from openai import OpenAI
 
client = OpenAI()
 
response = client.chat.completions.create(
    model="gpt-4o",
    response_format={"type": "json_object"},
    messages=[
        {
            "role": "system",
            "content": "You extract product information and return it as JSON with fields: name, price, category, description."
        },
        {
            "role": "user",
            "content": "The new AirPods Pro 3 cost $249 and feature hearing health capabilities with improved noise cancellation."
        }
    ]
)
 
data = json.loads(response.choices[0].message.content)
print(data)
# {"name": "AirPods Pro 3", "price": 249, "category": "audio", "description": "..."}

Important: You must mention "JSON" in your system or user message when using JSON mode, or the API will return an error.

Step 2: Schema-Strict Structured Output with OpenAI

For production applications, you want schema enforcement. OpenAI's structured outputs with json_schema guarantee the response matches your exact schema.

from openai import OpenAI
from pydantic import BaseModel
 
class ProductInfo(BaseModel):
    name: str
    price: float
    currency: str
    category: str
    features: list[str]
    in_stock: bool
 
client = OpenAI()
 
response = client.beta.chat.completions.parse(
    model="gpt-4o",
    response_format=ProductInfo,
    messages=[
        {
            "role": "system",
            "content": "Extract product information from the user's text."
        },
        {
            "role": "user",
            "content": "The Sony WH-1000XM6 headphones ($348) feature 40-hour battery life, multipoint connection, and improved ANC. Currently available."
        }
    ]
)
 
product = response.choices[0].message.parsed
print(product.name)       # "Sony WH-1000XM6"
print(product.features)   # ["40-hour battery life", "multipoint connection", "improved ANC"]

This approach uses Pydantic models to define your schema. The SDK handles converting your model to a JSON schema and parsing the response back into a typed object.

Step 3: Structured Output with Claude (Anthropic)

Anthropic's Claude supports structured output through tool use. You define a tool with your desired output schema, and the model "calls" that tool with structured data.

import anthropic
 
client = anthropic.Anthropic()
 
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[
        {
            "name": "extract_product",
            "description": "Extract structured product information",
            "input_schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "price": {"type": "number"},
                    "currency": {"type": "string"},
                    "category": {"type": "string"},
                    "features": {
                        "type": "array",
                        "items": {"type": "string"}
                    },
                    "in_stock": {"type": "boolean"}
                },
                "required": ["name", "price", "currency", "category", "features", "in_stock"]
            }
        }
    ],
    tool_choice={"type": "tool", "name": "extract_product"},
    messages=[
        {
            "role": "user",
            "content": "Extract product info: The Sony WH-1000XM6 headphones ($348) feature 40-hour battery life, multipoint connection, and improved ANC. Currently available."
        }
    ]
)
 
# The structured data is in the tool use block
tool_use_block = next(
    block for block in response.content if block.type == "tool_use"
)
product_data = tool_use_block.input
print(product_data["name"])      # "Sony WH-1000XM6"
print(product_data["features"])  # ["40-hour battery life", ...]

The key parameter is tool_choice — setting it to a specific tool name forces the model to respond with structured data matching that tool's schema rather than free-form text.

Step 4: Handling Nested and Complex Schemas

Real applications often need nested structures. Both APIs handle this well.

# OpenAI with nested Pydantic models
class Address(BaseModel):
    street: str
    city: str
    country: str
    postal_code: str
 
class ContactInfo(BaseModel):
    email: str
    phone: str | None
    address: Address
 
class CompanyProfile(BaseModel):
    name: str
    industry: str
    founded_year: int
    contacts: list[ContactInfo]
    annual_revenue_usd: float | None
# Claude with nested JSON schema
nested_schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "industry": {"type": "string"},
        "founded_year": {"type": "integer"},
        "contacts": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "email": {"type": "string"},
                    "phone": {"type": ["string", "null"]},
                    "address": {
                        "type": "object",
                        "properties": {
                            "street": {"type": "string"},
                            "city": {"type": "string"},
                            "country": {"type": "string"},
                            "postal_code": {"type": "string"}
                        },
                        "required": ["street", "city", "country", "postal_code"]
                    }
                },
                "required": ["email", "address"]
            }
        }
    },
    "required": ["name", "industry", "founded_year", "contacts"]
}

Step 5: Error Handling and Edge Cases

Even with structured output, you should handle edge cases gracefully.

import json
from openai import OpenAI
 
client = OpenAI()
 
def extract_with_fallback(text: str) -> dict:
    try:
        response = client.beta.chat.completions.parse(
            model="gpt-4o",
            response_format=ProductInfo,
            messages=[
                {"role": "system", "content": "Extract product information."},
                {"role": "user", "content": text}
            ]
        )
 
        # Check for refusal
        if response.choices[0].message.refusal:
            return {"error": "Model refused", "reason": response.choices[0].message.refusal}
 
        return response.choices[0].message.parsed.model_dump()
 
    except Exception as e:
        return {"error": "Extraction failed", "reason": str(e)}

For Claude, check that the stop reason is tool_use rather than end_turn, which would indicate the model responded with text instead of using the tool.

Practical Tips

Choose the right approach for your use case:

  • Use OpenAI's json_object mode for quick prototypes where you trust the model to follow instructions
  • Use OpenAI's json_schema mode for production apps that need guaranteed schema compliance
  • Use Claude's tool-use pattern when you want structured output combined with reasoning

Performance considerations:

  • Structured output adds minimal latency compared to free-form responses
  • Complex schemas with many nested objects may increase token usage
  • Both APIs support streaming with structured output — useful for large responses

Schema design tips:

  • Keep schemas as flat as possible for better reliability
  • Use descriptive field names — the model uses them as hints
  • Add description fields to your schema properties when the field name alone is ambiguous
  • Mark fields as optional (nullable) when the information might not be present in the input

Testing structured output:

  • Always test with edge cases: missing data, ambiguous input, multiple valid interpretations
  • Validate outputs beyond schema compliance — check that extracted values are semantically correct
  • Log and monitor extraction quality in production

Try openai

Get started with OpenAI's structured output API. New accounts receive free credits to experiment with JSON mode and schema-strict responses.

Try OpenAI API

Try claude

Anthropic's Claude offers structured output through its tool-use interface, with strong performance on complex extraction tasks.

Try Claude API