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.
Search across all documentation pages
Constrain an agent's final response to a typed Pydantic model so callers receive validated data instead of free text.
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.
BaseModel with types, Field(description=...), and enums for closed sets.output_type on Agent (Pydantic AI) or response_model (Instructor).NativeOutput or PromptedOutput for a specific model.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.
Include fields that drive:
Exclude:
| 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.
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.
Models read JSON Schema descriptions. Write them as operator instructions ("ISO-8601 date", "use service catalog names"), not as internal comments.
The same BaseModel can back:
output_type=IncidentTicketresponse_model=IncidentTicketKeep domain models in a shared module so extraction jobs and agents do not drift.
Pydantic AI can stream partial structured objects with partial validation. Only treat the final validated object as commit-ready for side effects.
sev1 vs critical) cause avoidable validation loops.ValueError("invalid") does not teach the model how to repair.str in output unions changes stop behavior. Including plain text makes free-text a valid final result; use deliberately.| 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 |
Use one when software will consume the result. Pure conversational UX can stay on text, but still structure tool args and any internal decisions.
If golden-set first-try success drops or retries dominate cost, the schema is too wide. Prefer nested sub-models or staged extraction.
Yes. Pydantic AI supports scalars, lists, dicts, dataclasses, TypedDicts, and models. Non-object schemas are wrapped so tool schemas remain objects.
Use Pydantic validators or @agent.output_validator and raise ModelRetry with a repair message (subject to retry budgets).
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).
Name models explicitly, add fields as optional first, and dual-write during migrations. Never silently rename fields that analytics or other agents depend on.
Defaults document optionality and help partial streams, but do not rely on defaults to paper over missing critical data.
You can, but agent schemas should stay decision-oriented. Blindly reusing entire API resources often creates unusable mega-models.
Instructions set role and policy. Schema sets the data contract. Put closed sets and required keys in the schema, not only in prose.
Use Pydantic AI's test models / function models, unit-test validators directly, and keep a small live golden suite for schema-model fit.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026