Skip to content

Structured Outputs

Generate reliable, schema-conformant JSON outputs using OpenAI's structured output feature with Pydantic models.

Overview

Structured Outputs ensure LLM responses conform to a predefined JSON schema, eliminating parsing errors and enabling type-safe integrations.

┌─────────────────────────────────────────────────────────────────────┐
│                    STRUCTURED OUTPUTS FLOW                           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  DEFINE SCHEMA              CALL API                PARSE RESPONSE  │
│  ┌─────────────┐           ┌─────────────┐         ┌─────────────┐  │
│  │  Pydantic   │           │             │         │  Validated  │  │
│  │   Model     │──────────►│   OpenAI    │────────►│   Python    │  │
│  │             │           │    API      │         │   Object    │  │
│  │ class User: │           │             │         │             │  │
│  │   name: str │   strict  │ { "name":   │  auto   │ user.name   │  │
│  │   age: int  │   =true   │   "John",   │  parse  │ user.age    │  │
│  └─────────────┘           │   "age": 30 │         └─────────────┘  │
│                            │ }           │                          │
│                            └─────────────┘                          │
│                                                                      │
│  ✅ GUARANTEES:                                                      │
│  ├─► Valid JSON syntax (no parsing errors)                          │
│  ├─► Schema conformance (correct types)                             │
│  ├─► Required fields present                                        │
│  └─► No extra fields (strict mode)                                  │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Basic Usage with Pydantic

Simple Model

python
from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class User(BaseModel):
    name: str
    age: int
    email: str

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Extract user information from the text."},
        {"role": "user", "content": "John Smith is 30 years old. Contact him at john@example.com"}
    ],
    response_format=User
)

user = response.choices[0].message.parsed
print(f"Name: {user.name}")   # John Smith
print(f"Age: {user.age}")      # 30
print(f"Email: {user.email}")  # john@example.com

Nested Models

python
from typing import Optional
from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    state: str
    zip_code: str
    country: str = "USA"

class Company(BaseModel):
    name: str
    industry: str
    founded_year: Optional[int] = None

class Person(BaseModel):
    name: str
    age: int
    address: Address
    employer: Optional[Company] = None
    skills: list[str]

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "user", "content": """
        Sarah Johnson, 28, lives at 123 Main St, San Francisco, CA 94102.
        She works at TechCorp (founded 2015) in the software industry.
        Her skills include Python, JavaScript, and machine learning.
        """}
    ],
    response_format=Person
)

person = response.choices[0].message.parsed
print(f"{person.name} works at {person.employer.name}")
print(f"Location: {person.address.city}, {person.address.state}")
print(f"Skills: {', '.join(person.skills)}")

Advanced Patterns

Chain-of-Thought with Structured Output

python
class Step(BaseModel):
    explanation: str
    output: str

class MathReasoning(BaseModel):
    steps: list[Step]
    final_answer: str

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "You are a math tutor. Show your reasoning step by step."},
        {"role": "user", "content": "If a train travels 120 miles in 2 hours, then slows down and travels 90 miles in 3 hours, what is the average speed for the entire journey?"}
    ],
    response_format=MathReasoning
)

reasoning = response.choices[0].message.parsed
for i, step in enumerate(reasoning.steps, 1):
    print(f"Step {i}: {step.explanation}")
    print(f"  → {step.output}\n")
print(f"Final Answer: {reasoning.final_answer}")

Enum Constraints

python
from enum import Enum
from pydantic import BaseModel

class Sentiment(str, Enum):
    POSITIVE = "positive"
    NEGATIVE = "negative"
    NEUTRAL = "neutral"

class Priority(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"

class TicketClassification(BaseModel):
    category: str
    sentiment: Sentiment
    priority: Priority
    summary: str
    suggested_action: str

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Classify support tickets."},
        {"role": "user", "content": "My order hasn't arrived and it's been 2 weeks! This is unacceptable!"}
    ],
    response_format=TicketClassification
)

ticket = response.choices[0].message.parsed
print(f"Priority: {ticket.priority.value}")  # Guaranteed to be one of the enum values
print(f"Sentiment: {ticket.sentiment.value}")

Union Types for Variable Responses

python
from typing import Union, Literal
from pydantic import BaseModel

class SuccessResponse(BaseModel):
    status: Literal["success"]
    data: dict
    message: str

class ErrorResponse(BaseModel):
    status: Literal["error"]
    error_code: str
    error_message: str

class APIResponse(BaseModel):
    response: Union[SuccessResponse, ErrorResponse]

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Parse API responses."},
        {"role": "user", "content": '{"status": "error", "code": "404", "msg": "User not found"}'}
    ],
    response_format=APIResponse
)

result = response.choices[0].message.parsed
if result.response.status == "error":
    print(f"Error: {result.response.error_message}")

JSON Schema (Direct)

For cases where you don't want to use Pydantic:

python
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Extract event information."},
        {"role": "user", "content": "The conference is on March 15, 2025 at 2pm in New York."}
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "event_extraction",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "event_name": {"type": "string"},
                    "date": {"type": "string", "description": "ISO 8601 date"},
                    "time": {"type": "string", "description": "24-hour format"},
                    "location": {
                        "type": "object",
                        "properties": {
                            "city": {"type": "string"},
                            "venue": {"type": ["string", "null"]}
                        },
                        "required": ["city"],
                        "additionalProperties": False
                    }
                },
                "required": ["event_name", "date", "time", "location"],
                "additionalProperties": False
            }
        }
    }
)

import json
event = json.loads(response.choices[0].message.content)

Strict Mode

The strict: true parameter ensures the model output matches the schema exactly:

python
# With strict mode (recommended for production)
response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[...],
    response_format=MyModel  # Pydantic automatically uses strict mode
)

# Manual JSON schema with strict mode
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[...],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "my_schema",
            "strict": True,  # Enable strict mode
            "schema": {...}
        }
    }
)

Strict Mode Requirements

RequirementDescription
additionalProperties: falseMust be set on all objects
All fields requiredUse optional types with defaults instead
No $refInline all definitions
Supported types onlystring, number, integer, boolean, array, object, null

Function Calling with Structured Outputs

Combine tool use with structured outputs:

python
from pydantic import BaseModel, Field

class WeatherParams(BaseModel):
    location: str = Field(description="City name")
    unit: Literal["celsius", "fahrenheit"] = "celsius"

class WeatherResult(BaseModel):
    location: str
    temperature: float
    unit: str
    conditions: str
    humidity: int

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a location",
            "parameters": WeatherParams.model_json_schema(),
            "strict": True
        }
    }
]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "user", "content": "What's the weather in Tokyo?"}
    ],
    tools=tools,
    tool_choice="auto"
)

# Parse tool call arguments
if response.choices[0].message.tool_calls:
    tool_call = response.choices[0].message.tool_calls[0]
    params = WeatherParams.model_validate_json(tool_call.function.arguments)
    print(f"Getting weather for: {params.location} in {params.unit}")

Common Patterns

Entity Extraction

python
class Entity(BaseModel):
    text: str
    type: Literal["person", "organization", "location", "date", "money"]
    confidence: float

class ExtractionResult(BaseModel):
    entities: list[Entity]
    raw_text: str

# Use for NER tasks

Classification

python
class Classification(BaseModel):
    category: str
    confidence: float
    reasoning: str
    alternative_categories: list[str]

# Multi-label classification
class MultiLabelClassification(BaseModel):
    labels: list[str]
    confidences: dict[str, float]

Data Transformation

python
class InputFormat(BaseModel):
    raw_data: str

class OutputFormat(BaseModel):
    cleaned_data: dict
    transformations_applied: list[str]
    validation_errors: list[str]

Error Handling

Parse Failures

python
from openai import OpenAI

client = OpenAI()

try:
    response = client.beta.chat.completions.parse(
        model="gpt-4o",
        messages=[...],
        response_format=MyModel
    )

    if response.choices[0].message.refusal:
        # Model refused to generate (safety reasons)
        print(f"Refused: {response.choices[0].message.refusal}")
    else:
        result = response.choices[0].message.parsed
        # Use result...

except Exception as e:
    print(f"Error: {e}")

Validation with Pydantic

python
from pydantic import BaseModel, Field, field_validator

class StrictUser(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    age: int = Field(ge=0, le=150)
    email: str

    @field_validator('email')
    @classmethod
    def validate_email(cls, v):
        if '@' not in v:
            raise ValueError('Invalid email format')
        return v

# Pydantic validation runs after OpenAI parsing

Performance Tips

Caching Schema

python
# Pre-compute JSON schema once
MY_SCHEMA = MyModel.model_json_schema()

# Reuse in multiple calls
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[...],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "my_model",
            "strict": True,
            "schema": MY_SCHEMA
        }
    }
)

Smaller Models for Simple Schemas

python
# Use gpt-4o-mini for simple extractions
response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",  # Cheaper, faster
    messages=[...],
    response_format=SimpleModel
)

# Use gpt-4o for complex nested schemas
response = client.beta.chat.completions.parse(
    model="gpt-4o",  # Better at complex structures
    messages=[...],
    response_format=ComplexNestedModel
)

Comparison with JSON Mode

FeatureStructured OutputsJSON Mode
Schema enforcement✅ Guaranteed❌ Best effort
Strict typing✅ Yes❌ No
Nested objects✅ Full support⚠️ May vary
Auto-parsing✅ With Pydantic❌ Manual JSON parse
Use caseProduction systemsQuick prototypes
python
# JSON Mode (old way) - no schema guarantee
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[...],
    response_format={"type": "json_object"}  # Just ensures valid JSON
)

# Structured Outputs (recommended) - schema guaranteed
response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[...],
    response_format=MyModel  # Guarantees schema conformance
)

See Also

Based on OpenAI Cookbook - Bain & OpenAI Collaboration