Pydantic v3 JSON Output Guide: Structured LLM Responses in 2026
Pydantic v3 is 10-30x faster than v2 with native streaming support for LLM structured output. Learn how to use Pydantic v3 models, allow_partial streaming, and fix common validation errors.
Have broken JSON right now? Fix it free in under 1 second — no signup.
Fix My JSON →allow_partial, and works across 20+ LLM providers through PydanticAI.
If you're still parsing LLM JSON by hand or running v2 in production, this guide covers the upgrade path, streaming patterns, and the validation errors you'll actually hit.
What Changed in Pydantic v3
Pydantic v3 (released mid-2026) introduced three changes that matter for LLM workflows:
1. 10-30x performance improvementThe core validation engine was rewritten in Rust, dramatically reducing the overhead of validating high-frequency structured outputs.
2.allow_partial for streaming
Streaming LLM responses now populate Pydantic models field-by-field. You can consume validated partial objects before the full response completes.
3. Native PydanticAI integrationPydanticAI v2 (stable June 2026) builds directly on v3, with type-safe agent output, 20+ provider support, and a validation retry budget.
Basic Structured Output with Pydantic v3
The core pattern is unchanged from v2:
from pydantic import BaseModel
from anthropic import Anthropic
class ExtractedData(BaseModel):
title: str
tags: list[str]
summary: str
confidence: float
client = Anthropic()
def extract_structured(text: str) -> ExtractedData:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system="Extract structured data. Respond with valid JSON only.",
messages=[{"role": "user", "content": text}]
)
raw = response.content[0].text
return ExtractedData.model_validate_json(raw)
model_validate_json() is v3's preferred method — it parses and validates in one step, and in v3 it's significantly faster than model_validate(json.loads(...)).
Streaming with allow_partial
The allow_partial flag is the key v3 addition for streaming workflows. It lets you consume a model as it populates, rather than waiting for the complete response:
from pydantic import BaseModel
from pydantic.experimental.partial import create_partial_model
class Report(BaseModel):
title: str
sections: list[str]
conclusion: str
word_count: int
PartialReport = create_partial_model(Report)
def stream_report(prompt: str):
# Accumulate streaming chunks
buffer = ""
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}]
) as stream:
for text in stream.text_stream:
buffer += text
# Try to parse partial JSON on each chunk
try:
partial = PartialReport.model_validate_json(buffer)
if partial.title:
print(f"Title: {partial.title}")
if partial.sections:
print(f"Sections so far: {partial.sections}")
except Exception:
pass # Buffer not yet parseable — keep accumulating
# Final complete validation
return Report.model_validate_json(buffer)
In practice, most production implementations use Instructor's create_partial() which handles the buffer management:
import instructor
from anthropic import Anthropic
from pydantic import BaseModel
client = instructor.from_anthropic(Anthropic())
class SummaryOutput(BaseModel):
key_points: list[str]
verdict: str
score: int
# Streams partial SummaryOutput objects as they populate
for partial in client.messages.create_partial(
model="claude-sonnet-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Summarize: [article text]"}],
response_model=SummaryOutput,
):
if partial.key_points:
print(partial.key_points)
PydanticAI for Multi-Provider Agents
PydanticAI v2 (stable June 2026) is the preferred way to build type-safe agents when you need structured output across providers:
from pydantic import BaseModel
from pydantic_ai import Agent
class AnalysisResult(BaseModel):
sentiment: str
entities: list[str]
confidence: float
reasoning: str
agent = Agent(
"claude-sonnet-5",
output_type=AnalysisResult,
system_prompt="Analyze the text and return structured analysis.",
)
async def run_analysis(text: str) -> AnalysisResult:
result = await agent.run(text)
return result.output # Typed as AnalysisResult
PydanticAI v2 adds a validation retry budget: if the LLM returns invalid JSON, it automatically retries with the validation error included in the prompt. You set the budget:
agent = Agent(
"claude-sonnet-5",
output_type=AnalysisResult,
retries=3, # Retry up to 3 times on validation failure
)
Common Validation Errors and Fixes
1. Markdown fences in LLM output
The model wraps JSON in a code block:
json
{"title": "Example", "score": 9}
Pydantic's model_validate_json() will fail on this. Strip fences before validating:
import re
def strip_fences(text: str) -> str:
text = text.strip()
if text.startswith(""):
text = re.sub(r'^```(?:json)?\s*', '', text)
text = re.sub(r'\s*```$', '', text)
return text.strip()
data = ExtractedData.model_validate_json(strip_fences(raw))
Or use [AI JSONMedic's repair endpoint](/json-repair) which strips fences as part of the repair pipeline.
### 2. Python boolean literals
Claude and other models occasionally produce Python-style booleans:json
{"active": True, "deleted": False, "value": None}
Fix before validating:python
def normalize_booleans(text: str) -> str:
text = text.replace("True", "true")
text = text.replace("False", "false")
text = text.replace("None", "null")
return text
### 3. Truncated JSON from token limits
If `max_tokens` is too low, the model stops mid-object. Detection:python
from pydantic import ValidationError
import json
def safe_validate(text: str, model_class):
try:
return model_class.model_validate_json(text)
except ValidationError as e:
# Check if it's a truncation issue
try:
json.loads(text)
except json.JSONDecodeError as je:
if "Expecting" in str(je) or "Unterminated" in str(je):
raise ValueError(f"JSON truncated at token limit: {je}")
raise e
Prevention: set `max_tokens` high enough and use streaming to detect when the model stops early.
### 4. Extra fields from creative models
Some models add fields you didn't ask for. Pydantic v3 ignores them by default, but if you want strict validation:python
class StrictOutput(BaseModel):
model_config = {"extra": "forbid"} # Raise on extra fields
title: str
score: int
### 5. Type coercion differences in v3
Pydantic v3 is stricter about coercion by default. In v2, `"42"` would coerce to `int(42)`. In v3:python
class Output(BaseModel):
count: int
v2: works (coerces "42" → 42)
v3: fails by default — LLM must return 42, not "42"
To restore v2 behavior:python
from pydantic import BaseModel
from pydantic.functional_validators import BeforeValidator
from typing import Annotated
CoercedInt = Annotated[int, BeforeValidator(lambda v: int(v) if isinstance(v, str) else v)]
class Output(BaseModel):
count: CoercedInt
---
## Choosing Between Libraries in 2026
| Library | Best For | v3 Support |
|---|---|---|
| **Pydantic v3 direct** | Simple structured output, high performance | Native |
| **Instructor** | Multi-provider, automatic retries, streaming | ✅ Full |
| **PydanticAI v2** | Type-safe agents, multi-step workflows | Native (built on v3) |
| **LangChain** | Existing LangChain stacks | ✅ v3 compatible |
For most new 2026 projects: **PydanticAI v2** for agents, **Instructor** for one-shot structured extraction.
---
## Performance: What 10-30x Faster Means in Practice
On high-throughput workloads (1,000+ validations/second), v3's Rust core makes a measurable difference:
- Validation latency drops from ~0.8ms to ~0.06ms per object
- Memory footprint is lower (Rust vs Python object overhead)
- No GIL contention in async workflows
For low-frequency workloads (occasional LLM calls, CLI tools), the difference is imperceptible. The upgrade is most valuable if you're running structured output pipelines at scale.
---
## Upgrading from Pydantic v2
Most v2 models work in v3 without changes. The main breaking changes:
1. **Stricter coercion** (see above — `str` → `int` no longer automatic)
2. **`model_validate_json()` preferred over `model_validate(json.loads(...))`**
3. **`create_partial_model` moved to `pydantic.experimental.partial`**
Run `pydantic migrate` for automated fixes:bash
pip install pydantic-v3
pydantic migrate ./src # Flags incompatible patterns
---
## Testing and Debugging Structured Output
### Unit testing Pydantic v3 modelspython
import pytest
from pydantic import ValidationError
from your_module import ExtractedData
def test_valid_output():
raw = '{"title": "Test", "tags": ["json", "api"], "summary": "A test.", "confidence": 0.95}'
result = ExtractedData.model_validate_json(raw)
assert result.title == "Test"
assert result.confidence == 0.95
def test_markdown_fence_handling():
raw = '``json\n{"title": "Test", "tags": [], "summary": "s", "confidence": 0.8}\n``'
# Your strip_fences function should handle this
cleaned = strip_fences(raw)
result = ExtractedData.model_validate_json(cleaned)
assert result.title == "Test"
def test_validation_error_on_missing_field():
raw = '{"title": "Test", "tags": []}' # Missing summary and confidence
with pytest.raises(ValidationError) as exc:
ExtractedData.model_validate_json(raw)
assert "summary" in str(exc.value)
### Logging validation failures for production debuggingpython
import logging
from pydantic import ValidationError
logger = logging.getLogger(__name__)
def validated_llm_call(prompt: str, model_class) -> dict:
response = llm_client.complete(prompt)
raw = response.text
try:
result = model_class.model_validate_json(strip_fences(raw))
return result
except ValidationError as e:
logger.error(
"LLM output validation failed",
extra={
"model": model_class.__name__,
"errors": e.errors(),
"raw_output": raw[:500], # Truncate for log size
"prompt_preview": prompt[:200],
}
)
raise
Logging the raw output alongside validation errors is essential for debugging — the error tells you what failed, the raw output tells you why. In production, track the failure rate by model and prompt pattern to identify systematic issues.
### Using AI JSONMedic as a fallback
When Pydantic validation fails and retries are exhausted, send the raw output to the repair API:python
import httpx
from pydantic import ValidationError
async def extract_with_fallback(text: str) -> ExtractedData:
response = await llm_client.complete(text)
raw = response.text
try:
return ExtractedData.model_validate_json(strip_fences(raw))
except (ValidationError, ValueError):
# Repair syntactic issues first, then try again
repair_response = await httpx.AsyncClient().post(
"https://aijsonmedic.com/api/repair",
json={"json": raw}
)
repaired = repair_response.json().get("repaired", raw)
return ExtractedData.model_validate_json(repaired)
```
This pattern handles syntactic errors (fences, commas, quotes) at the API level, while Pydantic handles semantic validation (wrong types, missing fields). For interactive debugging before writing repair code, paste the raw output into our JSON validator to see syntax errors line-by-line.
FAQ
Does Pydantic v3 work with all LLM providers?Pydantic v3 is provider-agnostic — it validates whatever JSON you give it. For multi-provider routing, use Instructor or PydanticAI v2 which abstract the provider API.
What's the difference between Instructor and PydanticAI?Instructor wraps provider SDKs to return Pydantic models from LLM calls. PydanticAI is a higher-level framework for building full agents with output types, tools, and retry budgets. PydanticAI uses Instructor patterns internally.
How do I handle LLM JSON that fails Pydantic validation?Try these in order: (1) Strip markdown fences, (2) Normalize Python booleans/None, (3) Use Instructor's automatic retry, (4) Send to AI JSONMedic's repair API for AI-assisted recovery.
Can I stream partial Pydantic models in real-time?Yes — use create_partial_model from pydantic.experimental.partial or Instructor's create_partial(). Both populate model fields as streaming chunks arrive.
allow_partial API stable in v3?
allow_partial is in pydantic.experimental in the initial v3 release — the API may change. For production streaming, Instructor's implementation is the more stable interface.
Still dealing with broken JSON?
Paste it in and get it fixed in under 1 second — free, no signup, no install. Works with ChatGPT, Claude, n8n, and any AI output.
Fix My JSON Free →Related Articles