Structured Outputs for Agents: JSON Schema Validation with Pydantic
Ensure reliable agent outputs with JSON Schema validation using Pydantic. Learn to define typed tool outputs, validate LLM responses, and build production-grade structured output pipelines.
Published on • September 6, 2026
AI Assistant

LLMs generate text. Your applications need structured data. Structured outputs bridge this gap by constraining LLM responses to conform to a defined JSON Schema, ensuring every output is valid, parseable, and ready for downstream processing.
Pydantic is the de facto standard for this in Python, providing type-safe data validation that integrates directly with LLM tool definitions and response schemas.
In this tutorial, you will learn how to define structured outputs for agents, validate tool calls with Pydantic, and build reliable agent pipelines that produce deterministic, schema-compliant results.
Why Structured Outputs
Without structured outputs, you face:
- Parsing failures: LLMs sometimes return malformed JSON
- Type errors: Numbers as strings, missing fields, wrong types
- Hallucinated fields: Output containing fields you didn’t request
- Retry loops: Wasting tokens on invalid responses
Structured outputs solve these problems by:
- Schema enforcement: The model is constrained to produce valid JSON
- Type validation: Pydantic validates every field against your schema
- Deterministic parsing: No regex or string manipulation needed
- Error handling: Clear validation errors for debugging
Defining Output Schemas with Pydantic
Pydantic models define the expected structure of LLM outputs:
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum
class Sentiment(str, Enum):
POSITIVE = "positive"
NEGATIVE = "negative"
NEUTRAL = "neutral"
class AnalysisResult(BaseModel):
sentiment: Sentiment
confidence: float = Field(ge=0.0, le=1.0)
key_topics: list[str]
summary: str
action_items: Optional[list[str]] = None
# Generate JSON Schema
schema = AnalysisResult.model_json_schema()
print(schema)
This produces a JSON Schema that can be passed directly to LLM providers:
{
"type": "object",
"properties": {
"sentiment": {"enum": ["positive", "negative", "neutral"]},
"confidence": {"type": "number", "minimum": 0.0, "maximum": 1.0},
"key_topics": {"type": "array", "items": {"type": "string"}},
"summary": {"type": "string"},
"action_items": {"type": "array", "items": {"type": "string"}, "nullable": true}
},
"required": ["sentiment", "confidence", "key_topics", "summary"]
}
Using Structured Outputs in Agents
Tool Output Validation
Define tool outputs with Pydantic schemas:
from agents import Agent, function_tool
from pydantic import BaseModel
class WeatherData(BaseModel):
temperature: float
condition: str
humidity: int
wind_speed: float
@function_tool
def get_weather(city: str) -> WeatherData:
"""Get current weather for a city."""
return WeatherData(
temperature=72.0,
condition="sunny",
humidity=45,
wind_speed=8.5,
)
agent = Agent(
name="Weather assistant",
instructions="Use the get_weather tool to answer weather questions.",
tools=[get_weather],
)
The tool return value is automatically validated against the Pydantic schema. If the data doesn’t match, a validation error is raised before it reaches the model.
Response Schema Enforcement
Constrain the entire agent output to a schema:
from agents import Agent
from pydantic import BaseModel
class TaskPlan(BaseModel):
task: str
steps: list[str]
estimated_time_minutes: int
required_tools: list[str]
risk_level: str
agent = Agent(
name="Planner",
instructions="Create detailed task plans for user requests.",
output_type=TaskPlan,
)
The agent will always return a TaskPlan instance, with all fields validated.
Advanced Validation Patterns
Field Constraints
Use Pydantic’s Field for fine-grained validation:
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, le=10000)
description: str = Field(max_length=500)
tags: list[str] = Field(min_length=1, max_length=10)
sku: str = Field(pattern=r'^[A-Z]{2}-\d{4}$')
Custom Validators
Add custom validation logic:
from pydantic import BaseModel, field_validator
class EmailContact(BaseModel):
email: str
name: str
@field_validator('email')
@classmethod
def validate_email(cls, v):
if '@' not in v:
raise ValueError('Invalid email address')
return v.lower()
Nested Models
Compose complex schemas from simpler models:
class Address(BaseModel):
street: str
city: str
country: str
postal_code: str
class Customer(BaseModel):
name: str
email: str
address: Address
orders: list['Order']
class Order(BaseModel):
order_id: str
items: list['OrderItem']
total: float
class OrderItem(BaseModel):
product_id: str
quantity: int
price: float
Handling Validation Errors
When an LLM response doesn’t match the schema, handle it gracefully:
from pydantic import ValidationError
async def run_with_validation(agent, prompt):
try:
result = await agent.run(prompt)
return result
except ValidationError as e:
# Log the validation error
print(f"Validation failed: {e}")
# Optionally retry with error context
retry_prompt = f"{prompt}\n\nNote: Your previous response had errors: {e}"
return await agent.run(retry_prompt)
Streaming Structured Outputs
When streaming agent responses, structured outputs are still validated:
async for event in agent.run_streamed(prompt):
if event.type == "output_text":
# Text is validated as it streams
partial_result = parse_partial(event.text)
Tool Definition Schemas
Pydantic models automatically generate tool definitions for LLMs:
from agents import function_tool
from pydantic import BaseModel
class SearchQuery(BaseModel):
query: str
max_results: int = 10
filters: dict[str, str] | None = None
@function_tool
def search_documents(query: SearchQuery) -> list[dict]:
"""Search the document database."""
# Implementation here
pass
# The tool schema is automatically generated from SearchQuery
Comparison with JSON Schema
Pydantic generates JSON Schema automatically:
| Feature | Pydantic | Manual JSON Schema |
|---|---|---|
| Type safety | Compile-time checks | Runtime only |
| Validation | Automatic | Manual |
| Serialization | model_dump() | json.dumps() |
| Documentation | model_json_schema() | Manual |
| Refactoring | IDE support | None |
Best Practices
- Start simple: Define minimal schemas first, add constraints as needed
- Use enums: Constrain string fields to known values
- Make optional fields explicit: Use
OptionalorNonedefaults - Validate early: Check inputs before they reach the LLM
- Handle failures gracefully: Plan for validation errors in production
- Document schemas: Use Field descriptions for LLM context
Next Steps
- Explore Building Guardrails for output validation patterns
- Read about Agent-to-Database for structured data access
- Learn about Evals for Enterprise Agents to test structured outputs
Structured outputs turn unpredictable LLM text into reliable, validated data. With Pydantic, you get type safety, clear schemas, and automatic validation—all critical for production agent systems.