← All guides

Claude Structured Output: Getting Reliable JSON Every Time

How to get Claude to reliably return JSON and structured data — system prompt techniques, schema enforcement, validation patterns, and handling edge.

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


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.

AI Disclosure: Drafted with Claude Code; all patterns from production Claude API usage as of April 2026.

Tools and references