← Back to DevBytes

Structured Output Validation with OpenAI Agents SDK: Complete Guide

Introduction to Structured Output Validation

When building AI agents that interact with external systems, databases, or APIs, the ability to reliably extract structured data from model responses is critical. The OpenAI Agents SDK provides robust mechanisms for structured output validation, ensuring that agent responses conform to expected schemas before being passed downstream. This guide walks through everything you need to know to implement, validate, and harden structured outputs in production agent workflows.

What Is Structured Output Validation?

Structured output validation is the process of constraining a language model's response to a specific data schema and verifying that the generated output matches that schema before it is consumed by your application. In the OpenAI Agents SDK, this is typically achieved by pairing a typed schema (such as a Pydantic model or a JSON Schema) with an agent's output type definition.

Validation serves two complementary purposes:

Why Structured Output Validation Matters

Without validation, agent outputs are free-form text. Downstream code that tries to parse JSON, populate database rows, or call APIs will inevitably break when the model returns prose, hallucinated fields, or incorrectly typed values. Structured output validation matters because it:

Core Concepts in the Agents SDK

Output Types

In the OpenAI Agents SDK, an agent can declare an output_type that defines the expected shape of its final response. When an output type is set, the SDK instructs the model to produce output matching that type and validates the result before returning it to the caller.

Pydantic Models

Pydantic is the most common way to define output schemas in Python. Pydantic models provide declarative field types, default values, custom validators, and automatic JSON Schema generation. The Agents SDK uses these schemas both to prompt the model and to validate responses.

Validators

Beyond basic type checking, Pydantic allows you to attach custom validators to fields or models. These validators run during parsing and can enforce business rules such as value ranges, string formats, or cross-field consistency.

Setting Up Your Environment

Before writing agents, install the Agents SDK and Pydantic. The examples below assume Python 3.10 or newer.

pip install openai-agents pydantic

Set your OpenAI API key as an environment variable:

export OPENAI_API_KEY="sk-your-key-here"

Defining a Structured Output Schema

The first step is to define the schema that represents the data you want the agent to produce. Use Pydantic models with explicit field types and helpful descriptions. Descriptions are important because they are included in the schema sent to the model and improve output quality.

from pydantic import BaseModel, Field
from typing import List, Optional


class Ingredient(BaseModel):
    name: str = Field(..., description="The name of the ingredient")
    quantity: str = Field(..., description="Amount with unit, e.g. '2 cups'")
    optional: bool = Field(False, description="Whether the ingredient is optional")


class Recipe(BaseModel):
    title: str = Field(..., description="A short, descriptive recipe title")
    servings: int = Field(..., ge=1, le=50, description="Number of servings")
    ingredients: List[Ingredient] = Field(..., description="List of ingredients")
    steps: List[str] = Field(..., description="Ordered preparation steps")
    prep_time_minutes: Optional[int] = Field(
        None, ge=0, description="Preparation time in minutes"
    )

Notice the use of Field constraints such as ge and le to enforce numeric bounds. These constraints are translated into the JSON Schema and used during validation.

Creating an Agent with a Structured Output Type

Once the schema is defined, attach it to an agent via the output_type parameter. The SDK will handle prompting the model to produce JSON conforming to the schema and will parse the response into the Pydantic model.

from agents import Agent, Runner


recipe_agent = Agent(
    name="RecipeAgent",
    instructions=(
        "You are a culinary assistant. Given a list of available ingredients "
        "and a cuisine preference, produce a complete recipe. Always return "
        "structured output matching the Recipe schema."
    ),
    model="gpt-4o-mini",
    output_type=Recipe,
)


async def generate_recipe(prompt: str) -> Recipe:
    result = await Runner.run(recipe_agent, prompt)
    return result.final_output

The result.final_output attribute will be an instance of Recipe, not a raw string. You can access its fields directly:

recipe = await generate_recipe(
    "I have chicken, rice, and coconut milk. I want Thai food."
)
print(recipe.title)
for ingredient in recipe.ingredients:
    print(f"- {ingredient.quantity} {ingredient.name}")

Adding Custom Validators

Type constraints alone are often insufficient. Custom validators let you enforce domain-specific rules. For example, you might require that the number of steps matches the complexity of the recipe, or that ingredient names are non-empty and trimmed.

from pydantic import field_validator, model_validator


class Recipe(BaseModel):
    title: str
    servings: int = Field(..., ge=1, le=50)
    ingredients: List[Ingredient]
    steps: List[str]
    prep_time_minutes: Optional[int] = Field(None, ge=0)

    @field_validator("title")
    @classmethod
    def title_must_not_be_empty(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("title must not be empty")
        return v.strip()

    @field_validator("steps")
    @classmethod
    def steps_must_be_non_empty(cls, v: List[str]) -> List[str]:
        cleaned = [s.strip() for s in v if s.strip()]
        if len(cleaned) != len(v):
            raise ValueError("steps must not contain empty entries")
        return cleaned

    @model_validator(mode="after")
    def check_ingredients_and_steps(self) -> "Recipe":
        if len(self.ingredients) == 0:
            raise ValueError("recipe must include at least one ingredient")
        if len(self.steps) == 0:
            raise ValueError("recipe must include at least one step")
        return self

When the model returns output that violates these rules, Pydantic raises a ValidationError. The Agents SDK can be configured to retry the request, optionally feeding the validation error back to the model so it can self-correct.

Handling Validation Failures with Retries

Validation failures are inevitable in production. A robust agent pipeline should catch these failures and retry. A common pattern is to wrap the runner call in a retry loop that includes the validation error message in the next prompt.

from agents import Runner
from pydantic import ValidationError


async def run_with_retries(agent, prompt: str, max_attempts: int = 3):
    current_prompt = prompt
    last_error = None

    for attempt in range(1, max_attempts + 1):
        try:
            result = await Runner.run(agent, current_prompt)
            return result.final_output
        except ValidationError as exc:
            last_error = exc
            current_prompt = (
                f"{prompt}\n\n"
                f"Your previous response failed validation with this error:\n"
                f"{exc}\n\n"
                f"Please correct the output and try again."
            )

    raise RuntimeError(
        f"Agent failed to produce valid output after {max_attempts} attempts: {last_error}"
    )

This approach leverages the model's ability to reason about its own mistakes. By including the structured error message, you give the model concrete feedback about what went wrong.

Using Output Guards

For more advanced validation, you can introduce an output guard: a separate agent or function that inspects the structured output before it is returned to the caller. Guards are useful when validation requires reasoning rather than simple rule checks.

from agents import Agent, Runner


safety_guard = Agent(
    name="SafetyGuard",
    instructions=(
        "You inspect a recipe for safety and feasibility. Reject recipes "
        "that include unsafe ingredients, impossible steps, or unrealistic "
        "prep times. Return a JSON object with 'approved' (boolean) and "
        "'reason' (string)."
    ),
    model="gpt-4o-mini",
    output_type=dict,
)


async def validate_with_guard(recipe: Recipe) -> bool:
    prompt = f"Inspect this recipe:\n{recipe.model_dump_json(indent=2)}"
    result = await Runner.run(safety_guard, prompt)
    verdict = result.final_output
    return verdict.get("approved", False)

Combine deterministic Pydantic validation with a reasoning-based guard to get the best of both worlds: fast, cheap checks for schema compliance and flexible checks for semantic correctness.

Working with Nested and Complex Schemas

Real-world agents often produce deeply nested structures. Pydantic handles nesting naturally, but you should keep schemas readable and avoid excessive depth. The following example models a customer support ticket analysis with nested entities.

from enum import Enum
from typing import List, Optional


class Sentiment(str, Enum):
    positive = "positive"
    neutral = "neutral"
    negative = "negative"


class ActionItem(BaseModel):
    description: str
    owner: Optional[str] = None
    priority: str = Field("medium", description="low, medium, or high")


class TicketAnalysis(BaseModel):
    summary: str = Field(..., description="One-sentence summary of the issue")
    sentiment: Sentiment
    category: str = Field(..., description="e.g. billing, technical, account")
    action_items: List[ActionItem] = Field(default_factory=list)
    requires_escalation: bool = False

    @field_validator("category")
    @classmethod
    def normalize_category(cls, v: str) -> str:
        return v.strip().lower()


support_agent = Agent(
    name="SupportAgent",
    instructions=(
        "Analyze customer support messages and return structured analysis. "
        "Identify sentiment, category, and concrete action items."
    ),
    model="gpt-4o-mini",
    output_type=TicketAnalysis,
)

Enums are particularly useful for constrained categorical fields. They are translated into JSON Schema enums, which strongly guide the model toward valid values.

Streaming Structured Outputs

When using streaming, partial structured outputs can be challenging because the JSON may be incomplete until the stream finishes. The Agents SDK provides incremental parsing utilities that allow you to validate partial results as they arrive, while still performing full validation at the end.

from agents import Runner


async def stream_structured(agent, prompt: str):
    result = Runner.run_streamed(agent, input=prompt)
    async for event in result.stream_events():
        if event.type == "response.output_text.delta":
            # Inspect partial text if needed for UI feedback
            pass
    # Final validation happens automatically when the stream completes
    return result.final_output

Avoid attempting to fully validate partial JSON mid-stream unless you have a specific need, such as progressive UI rendering. Rely on the SDK's final validation for correctness.

Best Practices

Testing Structured Outputs

Unit tests for your Pydantic models ensure that validation logic works independently of the model. These tests are fast, deterministic, and should be part of your CI pipeline.

import pytest
from pydantic import ValidationError


def test_valid_recipe():
    recipe = Recipe(
        title="Thai Chicken Curry",
        servings=4,
        ingredients=[Ingredient(name="chicken", quantity="1 lb")],
        steps=["Heat oil", "Add chicken", "Simmer"],
        prep_time_minutes=15,
    )
    assert recipe.servings == 4


def test_empty_title_rejected():
    with pytest.raises(ValidationError):
        Recipe(
            title="   ",
            servings=2,
            ingredients=[Ingredient(name="rice", quantity="1 cup")],
            steps=["Cook rice"],
        )


def test_servings_out_of_range():
    with pytest.raises(ValidationError):
        Recipe(
            title="Test",
            servings=0,
            ingredients=[Ingredient(name="rice", quantity="1 cup")],
            steps=["Cook"],
        )

Common Pitfalls

Conclusion

Structured output validation is the backbone of reliable agent systems. By defining clear Pydantic schemas, attaching meaningful validators, implementing retry logic, and combining deterministic checks with semantic guards, you can build agents that produce data your application can trust. The OpenAI Agents SDK makes this workflow straightforward, but the quality of your outputs ultimately depends on thoughtful schema design, clear instructions, and disciplined testing. Treat your output schemas as first-class contracts in your codebase, and your agents will integrate cleanly with the rest of your system.

— Ad —

Google AdSense will appear here after approval

← Back to all articles