# Using OpenAI Structured Output with Pydantic Models for Agents: A Complete Guide

> Learn to use OpenAI structured output with Pydantic models for agents. Enforce type safe LLM responses with this complete guide. Effortlessly validate and access typed Python objects.

- Repository: [Arindam Majumder /awesome-ai-apps](https://github.com/Arindam200/awesome-ai-apps)
- Tags: how-to-guide
- Published: 2026-05-06

---

**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`](https://github.com/Arindam200/awesome-ai-apps/blob/main/course/aws_strands/03_structured_output/main.py), the repository defines a schema for extracting person information:

```python
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`](https://github.com/Arindam200/awesome-ai-apps/blob/main/starter_ai_agents/pydantic_starter/main.py) using `OpenAIModel` and `OpenAIProvider`:

```python
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:

```python
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`](https://github.com/Arindam200/awesome-ai-apps/blob/main/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:

```python
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`](https://github.com/Arindam200/awesome-ai-apps/blob/main/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:

```python

# 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`](https://github.com/Arindam200/awesome-ai-apps/blob/main/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`](https://github.com/Arindam200/awesome-ai-apps/blob/main/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`](https://github.com/Arindam200/awesome-ai-apps/blob/main/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.