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.
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 & typedresult.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:
- 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.
- 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.
- 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:
- Write or capture one example response and format it to confirm the shape.
- Generate a JSON Schema from it, or convert it straight to typed Python to scaffold the model fields and types.
- 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.