Defining Agent Output Schemas with Pydantic Models
Constrain an agent's final response to a typed Pydantic model so callers receive validated data instead of free text.
Summary
Set Pydantic AI's output_type (or Instructor's response_model) to a BaseModel that encodes the fields, enums, and validators your application will act on.
Recipe
- List the fields a machine consumer needs (not every phrase the model might say).
- Model them as a Pydantic
BaseModelwith types,Field(description=...), and enums for closed sets. - Add validators for business rules that JSON Schema alone cannot express (cross-field checks, allowlists).
- Pass the model as
output_typeonAgent(Pydantic AI) orresponse_model(Instructor). - Prefer default tool-style structured output unless you need
NativeOutputorPromptedOutputfor a specific model. - Run once on golden examples, then tighten optional vs required fields based on real failure rates.
- Version the schema when consumers depend on it; treat breaking field renames like API breaks.
Working Example
from enum import Enum
from pydantic import BaseModel, Field, field_validator
from pydantic_ai import Agent
class Severity(str, Enum):
info = "info"
warning = "warning"
critical = "critical"
class IncidentTicket(BaseModel):
"""Final structured ticket the agent must return."""
title: str = Field(description="Short title for the incident board")
severity: Severity
affected_service: str
summary: str = Field(description="What happened and impact, 2-4 sentences")
next_actions: list[str] = Field(
min_length=1,
description="Concrete next steps for the on-call engineer",
)
page_oncall: bool = Field(
description="True only if severity is critical or customer impact is active"
)
@field_validator("title")
@classmethod
def title_not_empty(cls, v: str) -> str:
v = v.strip()
if len(v) < 5:
raise ValueError("title must be at least 5 characters")
return v
agent = Agent(
"openai:gpt-4o-mini", # verify at build
output_type=IncidentTicket,
instructions=(
"You are an incident triage agent. "
"Return only fields needed for the ticket system."
),
)
result = agent.run_sync(
"Payments API p99 latency is 4s for 12 minutes; checkout errors elevated."
)
ticket = result.output
print(ticket.severity, ticket.page_oncall)
print(ticket.model_dump_json(indent=2))Callers never parse markdown bullets.
They import IncidentTicket and type-check against result.output.
Deep Dive
What belongs in the schema
Include fields that drive:
- routing or severity
- tool selection or follow-up actions
- persistence and analytics
- UI binding (forms, badges, tables)
Exclude:
- chain-of-thought narration
- redundant natural-language restatements of structured fields
- unstable free-form keys ("extra notes" bags that become junk drawers)
Output modes in Pydantic AI
| Mode | Marker | Notes |
|---|---|---|
| Tool output (default) | plain type or ToolOutput(...) | Schema exposed as an output tool; works on most models |
| Native structured | NativeOutput(...) | Uses provider JSON-schema response formats where available |
| Prompted | PromptedOutput(...) | Schema in instructions; least forceful, sometimes more flexible |
Start with the default tool output path. Switch modes when a model cannot mix tools and native structured output, or when evals show better reliability for your schema.
Unions and multiple final shapes
When the agent may return different shapes (success vs need-more-info), use a list or union of models:
from pydantic import BaseModel
from pydantic_ai import Agent
class Ready(BaseModel):
answer: str
class NeedInfo(BaseModel):
question: str
agent = Agent(
"openai:gpt-4o-mini",
output_type=[Ready, NeedInfo],
instructions="If data is incomplete, return NeedInfo instead of guessing.",
)Each member becomes its own output tool by default, which often improves accuracy versus one giant optional-field model.
Field descriptions are part of the prompt
Models read JSON Schema descriptions. Write them as operator instructions ("ISO-8601 date", "use service catalog names"), not as internal comments.
Sharing schemas with Instructor
The same BaseModel can back:
- Pydantic AI
output_type=IncidentTicket - Instructor
response_model=IncidentTicket
Keep domain models in a shared module so extraction jobs and agents do not drift.
Streaming structured output
Pydantic AI can stream partial structured objects with partial validation. Only treat the final validated object as commit-ready for side effects.
Gotchas
- Optional everything hides failures. If every field is optional, almost any JSON "validates" and your contract is empty.
- Mega-schemas raise retry rates. Split into smaller outputs or multi-step agents when models thrash.
- Enums must match product language. Mismatched labels (
sev1vscritical) cause avoidable validation loops. - Native structured + tools is model-dependent. Some providers forbid combining them; verify for your model.
- Validators need clear errors. Vague
ValueError("invalid")does not teach the model how to repair. - Docstrings matter. Model and field docs are model-facing documentation, not only developer docs.
strin output unions changes stop behavior. Including plain text makes free-text a valid final result; use deliberately.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
Pydantic output_type | Typed, validated, agent-native | Framework learning curve |
Instructor response_model | Thin client, multi-provider | Not a full agent loop |
| Provider JSON mode only | Simple | Weak guarantees without your schema |
| Manual regex / fence parse | No deps | Brittle in production |
| TypedDict / dataclass outputs | Lighter models | Fewer validators than full Pydantic |
FAQs
Should every agent use a Pydantic output model?
Use one when software will consume the result. Pure conversational UX can stay on text, but still structure tool args and any internal decisions.
How many fields is too many?
If golden-set first-try success drops or retries dominate cost, the schema is too wide. Prefer nested sub-models or staged extraction.
Can output be a list or primitive?
Yes. Pydantic AI supports scalars, lists, dicts, dataclasses, TypedDicts, and models. Non-object schemas are wrapped so tool schemas remain objects.
How do I reject a valid shape that is business-wrong?
Use Pydantic validators or @agent.output_validator and raise ModelRetry with a repair message (subject to retry budgets).
What is the difference between tools and output_type?
Tools are intermediate actions whose results return to the model. output_type defines the final value that ends the run (unless you deliberately allow text).
How should I version schemas in production?
Name models explicitly, add fields as optional first, and dual-write during migrations. Never silently rename fields that analytics or other agents depend on.
Do Field defaults help the model?
Defaults document optionality and help partial streams, but do not rely on defaults to paper over missing critical data.
Can I generate the schema from OpenAPI?
You can, but agent schemas should stay decision-oriented. Blindly reusing entire API resources often creates unusable mega-models.
Where do instructions stop and schema start?
Instructions set role and policy. Schema sets the data contract. Put closed sets and required keys in the schema, not only in prose.
How do I test output schemas without live LLM spend?
Use Pydantic AI's test models / function models, unit-test validators directly, and keep a small live golden suite for schema-model fit.
Related
- Pydantic AI & Instructor Basics - first structured runs
- Why Structured Output Matters More as Agents Get Autonomous - why contracts matter
- Handling Validation Failures and Retry Loops Gracefully - repair loops
- Type-Safe Tool Definitions with Pydantic AI - tool arg schemas
- Using Instructor for Schema-Constrained Outputs Across Providers - extraction-focused path
- Pydantic AI & Instructor Best Practices - design checklist
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.