pydantic-aipythonstructured-outputagentsai

Pydantic AI: Type-Safe Structured Output and Agents in Python

·9 min read·AI & JSON

Structured output as a first-class primitive

Pydantic AI is a Python agent framework from the team behind Pydantic — the validation library already used inside most Python LLM tooling. Its pitch is "FastAPI's developer experience for GenAI": type-safe, model-agnostic, production-minded. The core move is that structured output is baked in, not bolted on. You declare the shape you want as a Pydantic model and pass it as output_type; the framework converts it to a JSON Schema, sends that alongside the prompt, validates the reply, and retries automatically if a field is missing or mistyped.

python
from pydantic import BaseModel
from pydantic_ai import Agent

class SupportTicket(BaseModel):
    category: str
    priority: str          # "low" | "medium" | "high"
    summary: str
    needs_human: bool

agent = Agent("openai:gpt-5.2", output_type=SupportTicket)

result = agent.run_sync("My Pro plan renewal failed twice and nobody replied.")
print(result.output.priority)   # -> "high", already validated & typed

result.output is a real SupportTicket instance — not a dict you have to guard with .get(). If the model returns priority: "urgent" and your validator rejects it, Pydantic AI sends the validation error back to the model and asks it to fix the output before surfacing anything to you.

The three output modes

Pydantic AI picks how to enforce the schema based on what the model supports:

  1. Tool / structured-output mode — the schema is exposed as a tool the model must call. This is the most reliable and the default when the provider supports it.
  2. JSON mode — the provider's "return valid JSON" flag is enabled and the schema is described in the instructions. Works widely; slightly less strict than constrained tool calls.
  3. Prompted mode — for models with no native support, the schema goes into the prompt as instructions and it's up to the model to comply. Most flexible, least guaranteed — always keep validation on.

Because validation is the same Pydantic you'd use anywhere, you can attach custom validators (@field_validator) that enforce business rules the JSON Schema can't express — and a failure there triggers the same automatic retry.

When your output isn't a clean model

Sometimes the shape is dynamic and you can't write a fixed BaseModel. Pydantic AI's StructuredDict() helper attaches a JSON Schema to a dict[str, Any] subclass, so you get schema enforcement without a static class — useful when the fields come from user configuration or a database.

From sample to model, fast

The quickest way to get a correct output_type is to start from a realistic JSON sample and work backward:

  1. Write or capture one example response and format it to confirm the shape.
  2. Generate a JSON Schema from it, or convert it straight to typed Python to scaffold the model fields and types.
  3. Keep a couple of golden examples and diff new outputs against them when you change prompts or swap models.

For the wider context, compare approaches in Get JSON From LLMs and Validate LLM JSON Against a Schema.

Frequently asked questions

With the raw SDK you manually build the JSON Schema, call the API, parse the reply, validate, and hand-roll retries. Pydantic AI wraps all of that: you pass output_type, and schema generation, validation, and retry-on-error are automatic and provider-agnostic.

Pydantic validates the response; if it fails, the framework sends the validation error back to the model asking it to correct the output and retries, only raising an error if it still can't produce a valid object.

No. Pydantic AI is model-agnostic — the same Agent and output_type work across OpenAI, Anthropic, Google, and local models, with the framework choosing the best enforcement mode each supports.

Yes — Pydantic AI supports streaming validated partial output, so you can update a UI as fields arrive rather than waiting for the whole object.

Try JSON to Python

Generate a Pydantic BaseModel from any JSON sample to use as output_type.