Using OpenAI Structured Output with Pydantic Models for Agents: A Complete Guide
You can enforce type-safe LLM responses by passing a Pydantic model to the structured_output_model parameter when initializing an Agent, which automatically prompts the model for JSON, validates the output, and returns a typed Python object accessible via result.structured_output.
The awesome-ai-apps repository demonstrates how to combine OpenAI-compatible APIs with Pydantic validation to build robust AI agents that return structured, typed data instead of raw strings. By leveraging the pydantic_ai library with providers like Nebius Token Factory, developers can define strict output schemas that eliminate fragile string parsing and provide full IDE autocomplete support.
Defining the Pydantic Schema
First, create a Pydantic BaseModel that describes exactly what fields the LLM should return. Include Field descriptions to guide the model's extraction logic. In course/aws_strands/03_structured_output/main.py, the repository defines a schema for extracting person information:
from pydantic import BaseModel, Field
class PersonInfo(BaseModel):
"""A Pydantic model to represent structured information about a person."""
name: str = Field(..., description="The full name of the person.")
age: int = Field(..., description="The age of the person.")
occupation: str = Field(..., description="The current occupation of the person.")
This schema acts as a contract. When the agent runs, it automatically prompts the LLM to emit JSON matching these fields, then validates the response against this model.
Configuring the OpenAI-Compatible Provider
The repository uses the Nebius Token Factory endpoint, which implements the OpenAI API specification. Configure the model in starter_ai_agents/pydantic_starter/main.py using OpenAIModel and OpenAIProvider:
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.openai import OpenAIProvider
import os
from dotenv import load_dotenv
load_dotenv()
model = OpenAIModel(
model_name='meta-llama/Meta-Llama-3.1-70B-Instruct',
provider=OpenAIProvider(
base_url='https://api.tokenfactory.nebius.com/v1',
api_key=os.environ['NEBIUS_API_KEY']
)
)
This setup allows you to use any OpenAI-compatible endpoint while maintaining the exact same structured output workflow.
Initializing the Agent with Structured Output Support
Pass your Pydantic model to the structured_output_model argument when creating the Agent. This signals the framework to handle JSON schema generation and validation automatically:
from pydantic_ai import Agent
agent = Agent(
model=model,
system_prompt="You are an expert assistant that extracts structured information about people from text.",
structured_output_model=PersonInfo,
)
According to the source code in starter_ai_agents/pydantic_starter/main.py, the Agent class handles the complexity internally: it injects the schema into the system prompt, requests JSON output from the LLM, and prepares the validation pipeline.
Executing the Agent and Retrieving Typed Results
Invoke the agent with your input text. The result object provides a structured_output attribute containing the validated Pydantic instance:
text = "John Smith is a 30-year-old software engineer living in San Francisco."
result = agent.run_sync(text) # or agent(text) depending on sync/async needs
person_info: PersonInfo = result.structured_output
print(person_info.name) # → John Smith
print(person_info.age) # → 30
print(person_info.occupation) # → software engineer
As implemented in course/aws_strands/03_structured_output/main.py, if the LLM fails to emit parsable JSON, the framework raises a clear ValidationError or falls back to a text response with a helpful exception message, preventing silent data corruption.
Complete Implementation Example
Below is a minimal, production-ready script combining environment setup, provider configuration, and structured output extraction:
# structured_weather_agent.py
import os
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.common_tools.duckduckgo import duckduckgo_search_tool
load_dotenv()
# 1. Define the output schema
class WeatherReport(BaseModel):
location: str = Field(..., description="City name")
temperature_c: float = Field(..., description="Current temperature in Celsius")
condition: str = Field(..., description="Short weather description (e.g., sunny, rainy)")
# 2. Configure the LLM provider
model = OpenAIModel(
model_name='meta-llama/Meta-Llama-3.1-70B-Instruct',
provider=OpenAIProvider(
base_url='https://api.tokenfactory.nebius.com/v1',
api_key=os.getenv('NEBIUS_API_KEY')
)
)
# 3. Build the agent with structured output contract
agent = Agent(
model=model,
tools=[duckduckgo_search_tool()], # Optional: adds real-time data retrieval
system_prompt="You are a weather assistant. Return a JSON matching the WeatherReport schema.",
structured_output_model=WeatherReport,
)
# 4. Run and receive typed output
if __name__ == "__main__":
result = agent.run_sync("What's the weather in Paris right now?")
weather: WeatherReport = result.structured_output
print(f"{weather.location}: {weather.temperature_c}°C, {weather.condition}")
Summary
- Define schemas with Pydantic: Create
BaseModelsubclasses withFielddescriptions to establish type-safe contracts for LLM outputs. - Use OpenAI-compatible providers: Configure
OpenAIModelwithOpenAIProviderto connect to endpoints like Nebius Token Factory while maintaining standard API compatibility. - Pass
structured_output_model: Initialize theAgentwith this parameter to enable automatic JSON schema injection and response validation. - Access via
result.structured_output: Retrieve validated, typed objects that offer IDE autocomplete and runtime type safety. - Handle errors explicitly: The framework raises validation errors for malformed responses, eliminating silent failures common in manual parsing approaches.
Frequently Asked Questions
How does the agent handle invalid JSON responses from the LLM?
If the LLM returns malformed JSON or fields that don't match the schema, the pydantic_ai framework raises a Pydantic ValidationError with specific details about the mismatch. As shown in course/aws_strands/03_structured_output/main.py, you can catch these exceptions to implement fallback logic or retry mechanisms, ensuring your application handles edge cases gracefully rather than propagating corrupt data.
Can I use this pattern with other OpenAI-compatible providers besides Nebius?
Yes. The OpenAIProvider class accepts any base_url that implements the OpenAI API specification. You can substitute the Nebius endpoint with Azure OpenAI, OpenAI's official API, or local servers like Ollama or vLLM by changing the base_url and api_key parameters while keeping the structured_output_model pattern identical.
What is the difference between the pydantic_ai and strands implementations shown in the repository?
Both libraries follow the same core pattern but use different underlying frameworks. The pydantic_ai implementation in starter_ai_agents/pydantic_starter/main.py uses OpenAIModel and OpenAIProvider directly, while the strands implementation in course/aws_strands/03_structured_output/main.py uses LiteLLMModel as a unified interface. The strands approach offers broader model provider compatibility through LiteLLM, whereas pydantic_ai provides tighter integration with Pydantic-specific optimizations.
Do I need to manually prompt the model to return JSON?
No. When you provide the structured_output_model parameter, the Agent class automatically appends the JSON schema to the system prompt and instructs the model to return valid JSON matching that schema. You should not manually add "return JSON" instructions to your prompts, as the framework handles the formatting and parsing internally to ensure compatibility with the Pydantic validator.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →