Claude Structured Output: Getting Reliable JSON Every Time
Getting Claude to reliably return valid JSON requires three things: a JSON-only instruction in the system prompt, an example of the exact schema you want, and a validation step that handles the occasional malformed response. Without all three, Claude sometimes wraps JSON in markdown code blocks, adds explanatory text before or after, or returns slightly wrong field names. This guide covers the complete production pattern.
The simplest reliable JSON pattern
System prompt:
SYSTEM_PROMPT = """You are a data extraction assistant.
CRITICAL: You must respond with ONLY valid JSON. No explanation, no markdown
code blocks, no text before or after the JSON. Just the raw JSON object.
If you cannot extract the requested data, return: {"error": "reason"}
"""
User message with schema example:
USER_TEMPLATE = """Extract the following information from the text and return as JSON.
Required schema:
{{
"name": "string",
"email": "string or null",
"company": "string or null",
"phone": "string or null"
}}
Text to extract from:
{text}"""
Complete implementation:
import anthropic
import json
client = anthropic.Anthropic()
def extract_contact(text: str) -> dict:
"""Extract contact information from text. Returns dict with extracted fields."""
response = client.messages.create(
model="claude-haiku-4-5", # Haiku is sufficient for extraction
max_tokens=512,
system=SYSTEM_PROMPT,
messages=[{
"role": "user",
"content": USER_TEMPLATE.format(text=text)
}]
)
raw = response.content[0].text.strip()
# Remove markdown code blocks if present (defensive)
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"error": f"Invalid JSON: {raw[:100]}"}
# Test
result = extract_contact("Hi, I'm Jane Smith from Acme Corp. Email me at jane@acme.com")
print(result)
# {"name": "Jane Smith", "email": "jane@acme.com", "company": "Acme Corp", "phone": null}
Pydantic validation (Python)
For production use, validate the JSON against a schema:
from pydantic import BaseModel, EmailStr, validator
from typing import Optional
class ContactInfo(BaseModel):
name: str
email: Optional[str] = None
company: Optional[str] = None
phone: Optional[str] = None
@validator("email")
def validate_email(cls, v):
if v and "@" not in v:
return None # Invalid email — set to None rather than error
return v
def extract_contact_validated(text: str) -> ContactInfo | None:
raw = extract_contact(text)
if "error" in raw:
return None
try:
return ContactInfo(**raw)
except Exception:
return None
contact = extract_contact_validated("Contact Bob at bob@example.com, Widgets Inc")
if contact:
print(f"Name: {contact.name}, Email: {contact.email}")
Zod validation (TypeScript)
import { z } from "zod";
import Anthropic from "@anthropic-ai/sdk";
const ContactSchema = z.object({
name: z.string(),
email: z.string().email().nullable(),
company: z.string().nullable(),
phone: z.string().nullable(),
});
type Contact = z.infer<typeof ContactSchema>;
async function extractContact(text: string): Promise<Contact | null> {
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 512,
system: `Respond with ONLY valid JSON. Schema:
{"name": "string", "email": "string|null", "company": "string|null", "phone": "string|null"}`,
messages: [{ role: "user", content: `Extract contact from: ${text}` }],
});
const raw = response.content[0].type === "text"
? response.content[0].text.trim()
: "";
try {
const parsed = JSON.parse(raw);
return ContactSchema.parse(parsed);
} catch {
return null;
}
}
The "prefill" technique for higher reliability
Claude's API allows pre-filling the start of the assistant response. Starting the response with { forces Claude to complete a JSON object:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system="Extract data and return as JSON.",
messages=[
{"role": "user", "content": f"Extract product info from: {text}"},
# Pre-fill: Claude will complete this JSON object
{"role": "assistant", "content": "{"},
]
)
# Response will be the JSON content (after the leading "{")
# Prepend "{" to get the full JSON
raw_json = "{" + response.content[0].text
result = json.loads(raw_json)
The prefill technique nearly eliminates the "added markdown wrapper" problem because Claude is forced to continue the JSON you started.
Complex nested schemas
For complex structures, include a complete example in the prompt:
EXTRACTION_PROMPT = """Extract invoice data and return ONLY this JSON structure:
{{
"invoice_number": "string",
"date": "YYYY-MM-DD",
"vendor": {{
"name": "string",
"address": "string or null"
}},
"line_items": [
{{
"description": "string",
"quantity": number,
"unit_price": number,
"total": number
}}
],
"subtotal": number,
"tax": number,
"total": number
}}
IMPORTANT: All monetary values as numbers (not strings). Date as ISO 8601.
Return null for missing optional fields, not the string "null".
Invoice text:
{invoice_text}"""
Handling extraction failures gracefully
Claude will occasionally fail to extract information (genuinely missing data, ambiguous text). Build for this:
def extract_with_fallback(text: str, schema: dict) -> dict:
"""
Try to extract structured data. On failure, return partial data
with error notes rather than crashing.
"""
result = extract_contact(text)
# Fill in missing required fields with None rather than missing keys
for key in schema:
if key not in result:
result[key] = None
return result
# The caller always gets a dict with all expected keys
data = extract_with_fallback(partial_text, {"name": None, "email": None})
print(data["email"]) # None, never KeyError
Batch extraction (multiple items)
For extracting from many documents, use an array schema:
BATCH_SYSTEM = """Extract data from the provided documents and return a JSON array.
Each item in the array corresponds to one document.
Return ONLY the JSON array, no other text.
Schema for each item:
{"id": "string", "title": "string", "author": "string or null", "date": "YYYY-MM-DD or null"}
"""
def extract_batch(documents: list[dict]) -> list[dict]:
formatted = "\n\n".join([
f"Document {i+1} (id: {doc['id']}):\n{doc['content']}"
for i, doc in enumerate(documents)
])
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
system=BATCH_SYSTEM,
messages=[{"role": "user", "content": formatted}]
)
try:
return json.loads(response.content[0].text)
except json.JSONDecodeError:
return []
Frequently asked questions
Does Anthropic have a native JSON mode like OpenAI? Not as a separate API parameter (as of April 2026). The system prompt + schema example approach achieves equivalent reliability. The prefill technique gets you to >99% valid JSON. Anthropic has indicated structured output is on the roadmap.
How reliable is Claude's JSON output without validation? With the "JSON only" system prompt and a schema example: ~95% of responses parse correctly. With the prefill technique: ~99%. For production code, always validate — the 1% failure rate causes bugs at scale.
Can I use Claude to validate JSON it generated?
You could, but a Python json.loads() or zod.parse() is faster and more reliable for basic validation. Use Claude for semantic validation ("does this data make sense?") not syntax validation.
What's the best model for structured extraction? Claude Haiku for simple extraction (single level, clear fields). Claude Sonnet for complex nested schemas or ambiguous text. Haiku at $0.80/M input tokens is much cheaper for high-volume extraction pipelines.
Should I use XML instead of JSON for structured output? XML is an alternative, but JSON is more practical for most downstream use. Claude does sometimes produce more reliable XML than JSON for complex nested structures. If you're seeing persistent JSON formatting issues, try an XML schema — then convert to JSON in your code.
Related guides
- Claude Tool Use: Complete Guide to Function Calling — tool use for more complex structured interactions
- Claude API Error Handling: Production Patterns — handling malformed output gracefully
Take It Further
Claude Agent SDK Cookbook: 40 Production Patterns — Pattern 11 covers the complete Structured Output System: prefill technique, multi-schema extraction, streaming JSON, error recovery, and the validated extraction pipeline with automatic retry.
→ Get the Agent SDK Cookbook — $49
30-day money-back guarantee. Instant download.