If you have ever written json.loads(response) around an LLM call and then a defensive try/except because the model returned
```json
fences, a trailing comma, or prose before the object — this is for you.
The fix is to stop treating the model's output as text you parse, and start treating it as a typed object the library validates for you. With pydantic-ai you declare the shape once and get a validated Python object back, with a retry on the model when it doesn't conform.
from pydantic import BaseModel, Field
from pydantic_ai import Agent
class Invoice(BaseModel):
vendor: str
total: float = Field(..., description='Grand total in USD')
due_date: str | None = None
agent = Agent('anthropic:claude-sonnet-4-6', output_type=Invoice,
system_prompt='Extract the invoice fields from the text.')
result = agent.run_sync('Acme Corp — $1,240.00 due 2026-07-01')
print(result.output.total) # 1240.0 (a float, already validated)
What you no longer write: the JSON fence stripping, the KeyError guards, the float coercion, the "the model added an apology before the JSON" handling. If the model returns something that doesn't fit Invoice, pydantic-ai sends the validation error back to the model and asks it to try again — so your application code only ever sees a clean object.
Three things this buys you in production:
- The type is the contract. Your endpoint signature, your tests, and your prompt all agree, because they reference the same model.
-
Failures are explicit. A field that won't validate raises where you can catch it, instead of silently becoming
Nonethree functions later. -
It's auditable.
result.outputis a real object you can log, diff, and assert on.
The pattern scales from one field to a nested schema, and it's the same whether you're on Claude, GPT, or a local model. Once you've used it you stop writing parsers entirely.
I package patterns like this as small open-source pydantic-ai + FastAPI templates — the repos are on GitHub, and complete, ready-to-run versions are on Gumroad. Feedback and issues welcome.
Top comments (1)
The automatic-retry-on-validation-failure loop is the real win here, but it is worth being explicit that each retry feeds the validation error back as context, which costs tokens and can still converge on the same malformed shape if the schema is ambiguous. Two things that cut my retry rate to near zero: using discriminated unions so the model cannot blur two output variants, and adding field descriptions inside the Pydantic model since those flow into the JSON schema the model actually sees. Have you tested how pydantic-ai behaves when a required field is plausible but hallucinated, since a value that passes type validation but is factually wrong slips straight through?