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.comNested 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
| Requirement | Description |
|---|---|
additionalProperties: false | Must be set on all objects |
| All fields required | Use optional types with defaults instead |
No $ref | Inline all definitions |
| Supported types only | string, 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 tasksClassification
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 parsingPerformance 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
| Feature | Structured Outputs | JSON Mode |
|---|---|---|
| Schema enforcement | ✅ Guaranteed | ❌ Best effort |
| Strict typing | ✅ Yes | ❌ No |
| Nested objects | ✅ Full support | ⚠️ May vary |
| Auto-parsing | ✅ With Pydantic | ❌ Manual JSON parse |
| Use case | Production systems | Quick 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
)