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 BaseModel subclasses with Field descriptions to establish type-safe contracts for LLM outputs.
  • Use OpenAI-compatible providers: Configure OpenAIModel with OpenAIProvider to connect to endpoints like Nebius Token Factory while maintaining standard API compatibility.
  • Pass structured_output_model: Initialize the Agent with 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →