December 2025 Update: Master structured outputs with OpenAI’s JSON mode, Pydantic integration, and schema validation techniques.
Why Structured Outputs?
LLMs naturally output free-form text, but applications need structured data:Copy
Free-form Output Structured Output
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"The sentiment is positive {"sentiment": "positive",
and confidence is around "confidence": 0.92,
92%. The key topics are "topics": ["service", "quality"]}
service and quality."
| Method | Reliability | Flexibility | Best For |
|---|---|---|---|
| JSON Mode | High | Medium | Simple structures |
| Function Calling | Very High | High | Tool integration |
| Structured Outputs | Guaranteed | High | Complex schemas |
OpenAI JSON Mode
The simplest way to get JSON:Copy
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "Extract information and return valid JSON."
},
{
"role": "user",
"content": "John Smith, 35 years old, software engineer at Google"
}
],
response_format={"type": "json_object"}
)
import json
data = json.loads(response.choices[0].message.content)
print(data)
# {"name": "John Smith", "age": 35, "job": "software engineer", "company": "Google"}
JSON mode requires you to mention “JSON” in the prompt. Always include format instructions!
JSON Mode with Schema Instructions
Copy
def extract_with_schema(text: str, schema: dict) -> dict:
"""Extract structured data following a schema"""
schema_str = json.dumps(schema, indent=2)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": f"""Extract information from the text and return JSON matching this schema:
{schema_str}
Rules:
- Return ONLY valid JSON
- Include all required fields
- Use null for missing optional fields
- Match the exact field names and types"""
},
{"role": "user", "content": text}
],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
# Usage
schema = {
"name": "string",
"email": "string",
"age": "integer",
"interests": ["string"]
}
result = extract_with_schema(
"Jane Doe ([email protected]) is 28 and loves hiking and photography",
schema
)
OpenAI Structured Outputs (Guaranteed Schema)
For guaranteed schema compliance, use structured outputs:Copy
from pydantic import BaseModel
from typing import Optional, List
class Person(BaseModel):
name: str
age: int
email: Optional[str] = None
skills: List[str]
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Extract person information."},
{"role": "user", "content": "Bob is 30, knows Python and JavaScript, email: [email protected]"}
],
response_format=Person
)
person = response.choices[0].message.parsed
print(f"Name: {person.name}, Age: {person.age}")
print(f"Skills: {', '.join(person.skills)}")
Complex Nested Schemas
Copy
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class Task(BaseModel):
title: str = Field(description="Short task title")
description: str = Field(description="Detailed task description")
priority: Priority
estimated_hours: float = Field(ge=0, le=100)
class ProjectPlan(BaseModel):
project_name: str
objective: str
tasks: List[Task]
total_hours: float
risks: List[str]
dependencies: Optional[List[str]] = None
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{
"role": "system",
"content": "You are a project planning assistant. Create detailed project plans."
},
{
"role": "user",
"content": "Plan a website redesign project for a small business"
}
],
response_format=ProjectPlan
)
plan = response.choices[0].message.parsed
print(f"Project: {plan.project_name}")
for task in plan.tasks:
print(f" - [{task.priority.value}] {task.title}: {task.estimated_hours}h")
Instructor: Pydantic + LLMs
Instructor is the most popular library for structured outputs:Copy
pip install instructor
Copy
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List
# Patch OpenAI client
client = instructor.from_openai(OpenAI())
class ExtractedEntity(BaseModel):
name: str
entity_type: str = Field(description="person, organization, location, etc.")
context: str = Field(description="How this entity is mentioned")
class DocumentAnalysis(BaseModel):
summary: str = Field(description="One paragraph summary")
entities: List[ExtractedEntity]
sentiment: str = Field(description="positive, negative, or neutral")
key_topics: List[str]
language: str
# Use like normal, but with response_model
analysis = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "Analyze this article: Apple announced..."}
],
response_model=DocumentAnalysis
)
print(analysis.summary)
for entity in analysis.entities:
print(f" {entity.entity_type}: {entity.name}")
Instructor with Validation and Retries
Copy
from pydantic import BaseModel, Field, field_validator
from typing import List
import instructor
class ValidatedExtraction(BaseModel):
email: str
phone: str
website: str
@field_validator("email")
@classmethod
def validate_email(cls, v):
if "@" not in v:
raise ValueError("Invalid email format")
return v.lower()
@field_validator("phone")
@classmethod
def validate_phone(cls, v):
digits = "".join(c for c in v if c.isdigit())
if len(digits) < 10:
raise ValueError("Phone must have at least 10 digits")
return digits
client = instructor.from_openai(OpenAI())
# Instructor will retry if validation fails
result = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "Contact: [email protected], 555-123-4567, www.example.com"}
],
response_model=ValidatedExtraction,
max_retries=3 # Retry up to 3 times on validation failure
)
Streaming with Instructor
Copy
from pydantic import BaseModel
from typing import List
import instructor
class StoryChapter(BaseModel):
title: str
content: str
characters: List[str]
class Story(BaseModel):
title: str
chapters: List[StoryChapter]
client = instructor.from_openai(OpenAI())
# Stream partial objects
for partial in client.chat.completions.create_partial(
model="gpt-4o",
messages=[
{"role": "user", "content": "Write a 3 chapter short story about a robot"}
],
response_model=Story
):
# Partial object updates as it streams
if partial.title:
print(f"Title: {partial.title}")
if partial.chapters:
print(f"Chapters so far: {len(partial.chapters)}")
Function Calling for Structured Output
Use function calling when you need action + structured data:Copy
from openai import OpenAI
import json
client = OpenAI()
tools = [
{
"type": "function",
"function": {
"name": "create_calendar_event",
"description": "Create a calendar event",
"strict": True, # Enable strict mode
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Event title"
},
"start_time": {
"type": "string",
"description": "Start time in ISO format"
},
"end_time": {
"type": "string",
"description": "End time in ISO format"
},
"attendees": {
"type": "array",
"items": {"type": "string"},
"description": "List of attendee emails"
},
"location": {
"type": "string",
"description": "Event location"
}
},
"required": ["title", "start_time", "end_time"],
"additionalProperties": False
}
}
}
]
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "Schedule a team meeting tomorrow at 2pm for 1 hour with [email protected] and [email protected]"}
],
tools=tools,
tool_choice={"type": "function", "function": {"name": "create_calendar_event"}}
)
# Parse the structured function call
tool_call = response.choices[0].message.tool_calls[0]
event_data = json.loads(tool_call.function.arguments)
print(event_data)
Building a Structured Output Pipeline
Copy
from pydantic import BaseModel, Field
from typing import List, Optional, Any
from openai import OpenAI
import instructor
from enum import Enum
class OutputFormat(str, Enum):
JSON = "json"
PYDANTIC = "pydantic"
FUNCTION_CALL = "function_call"
class StructuredOutputPipeline:
"""Unified pipeline for structured outputs"""
def __init__(self, model: str = "gpt-4o"):
self.model = model
self.raw_client = OpenAI()
self.instructor_client = instructor.from_openai(OpenAI())
def extract(
self,
text: str,
schema: type[BaseModel],
system_prompt: str = "Extract information accurately.",
max_retries: int = 2
) -> BaseModel:
"""Extract structured data using Instructor"""
return self.instructor_client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
],
response_model=schema,
max_retries=max_retries
)
def extract_batch(
self,
texts: List[str],
schema: type[BaseModel],
system_prompt: str = "Extract information accurately."
) -> List[BaseModel]:
"""Extract from multiple texts"""
import asyncio
from openai import AsyncOpenAI
async_client = instructor.from_openai(AsyncOpenAI())
async def extract_one(text: str) -> BaseModel:
return await async_client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
],
response_model=schema
)
async def extract_all():
return await asyncio.gather(*[extract_one(t) for t in texts])
return asyncio.run(extract_all())
def extract_with_confidence(
self,
text: str,
schema: type[BaseModel]
) -> tuple[BaseModel, float]:
"""Extract with confidence score"""
class WithConfidence(BaseModel):
data: schema
confidence: float = Field(ge=0, le=1, description="Confidence in extraction accuracy")
reasoning: str = Field(description="Why this confidence level")
result = self.instructor_client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": "Extract information and rate your confidence (0-1) in the extraction accuracy."
},
{"role": "user", "content": text}
],
response_model=WithConfidence
)
return result.data, result.confidence
# Usage
pipeline = StructuredOutputPipeline()
class ProductReview(BaseModel):
product_name: str
rating: float = Field(ge=1, le=5)
pros: List[str]
cons: List[str]
recommendation: bool
review, confidence = pipeline.extract_with_confidence(
"The new iPhone 15 is amazing! Great camera, fast processor. "
"Battery could be better though. 4.5 stars, definitely recommend!",
ProductReview
)
print(f"Product: {review.product_name}")
print(f"Rating: {review.rating}/5")
print(f"Confidence: {confidence:.0%}")
Handling Edge Cases
Partial Extraction
Copy
from pydantic import BaseModel, Field
from typing import Optional, List
class FlexibleExtraction(BaseModel):
"""Schema that handles missing data gracefully"""
name: Optional[str] = Field(None, description="Person's name if mentioned")
email: Optional[str] = Field(None, description="Email if found")
phone: Optional[str] = Field(None, description="Phone if found")
extracted_fields: List[str] = Field(
default_factory=list,
description="List of fields that were successfully extracted"
)
missing_fields: List[str] = Field(
default_factory=list,
description="List of fields that could not be found"
)
extraction_notes: str = Field(
"",
description="Any notes about ambiguity or uncertainty"
)
Union Types for Variable Outputs
Copy
from pydantic import BaseModel
from typing import Union, Literal
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]
Key Takeaways
Use Structured Outputs
OpenAI’s structured outputs guarantee schema compliance
Pydantic is Essential
Pydantic provides validation, type safety, and clear schemas
Instructor for Production
Instructor adds retries, streaming, and validation on top of OpenAI
Plan for Edge Cases
Use Optional fields and flexible schemas for real-world data
What’s Next
LLM Caching
Learn caching strategies to reduce costs and latency