Structured Output Enforcement
JSON output, prefilling, schema validation, and retry loops.
Learning Objectives
- Enforce JSON output with prefilling technique
- Validate responses against JSON schemas
- Implement validation retry loops
Structured Output Enforcement
Production systems rarely consume Claude's output as free-form text. Downstream code needs to parse, validate, and act on the output programmatically. This lesson covers the techniques and patterns for reliably extracting structured data — primarily JSON — from Claude's responses. Mastering these patterns is essential because a system that returns valid JSON 95% of the time is a system that fails 5% of the time in production.
Requesting JSON Output
The most direct approach to getting structured output is to explicitly ask for it. Claude is highly capable of producing valid JSON when the request is clear and the expected schema is specified.
Basic JSON Request
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": (
"Extract the following information from this customer email "
"and return it as JSON.\n\n"
"<email>\n"
"Hi, my name is Sarah Chen and I purchased order #A1234 on "
"March 5th. The shipping address should be updated to "
"456 Oak Avenue, Portland, OR 97201. My phone number is "
"503-555-0147. Thanks!\n"
"</email>\n\n"
"<output_schema>\n"
"{\n"
" \"customer_name\": \"string\",\n"
" \"order_id\": \"string\",\n"
" \"request_type\": \"string\",\n"
" \"details\": {\n"
" \"new_address\": \"string\",\n"
" \"phone\": \"string\"\n"
" }\n"
"}\n"
"</output_schema>\n\n"
"Return ONLY the JSON object. No additional text or explanation."
)}]
)Prefilling: The Most Powerful Technique
Prefilling is a technique unique to the Anthropic API that gives you precise control over the start of Claude's response. By including an assistant message at the end of the messages array, you force Claude to continue from that exact point. This is the single most reliable technique for ensuring structured output.
How Prefilling Works
When you include a partial assistant message, Claude treats it as the beginning of its own response and continues from there. This means you can force Claude to start with an opening brace, an XML tag, or any other token that constrains the output format.
import anthropic
import json
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[
{"role": "user", "content": (
"Analyze the sentiment of this review and return a JSON "
"object with \"sentiment\" (positive/negative/neutral), "
"\"confidence\" (0-1), and \"key_phrases\" (list of strings).\n\n"
"<review>\n"
"The food was absolutely incredible but the service was "
"painfully slow. Would come back for the food alone.\n"
"</review>"
)},
{"role": "assistant", "content": "{"}
]
)
# The response continues from "{" — prepend it back
json_str = "{" + response.content[0].text
result = json.loads(json_str)
print(result)assistant role message at the end of the messages array, (2) the model continues from the prefilled text, (3) you must prepend the prefilled text back to the response to reconstruct the full output, and (4) prefilling is not compatible with Extended Thinking mode.Advanced Prefilling Patterns
Prefilling is not limited to a single character. You can prefill entire structures:
import anthropic
client = anthropic.Anthropic()
# Prefill to force a specific JSON structure
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[
{"role": "user", "content": (
"Extract all entities from the following text.\n\n"
"<text>Apple CEO Tim Cook announced the new iPhone 16 "
"at the Cupertino headquarters on September 9, 2024.</text>"
)},
{"role": "assistant", "content": (
"{\n \"entities\": ["
)}
]
)
# Reconstruct the full JSON
full_json = "{\n \"entities\": [" + response.content[0].textPrefilling for Non-JSON Formats
Prefilling works for any output format, not just JSON:
import anthropic
client = anthropic.Anthropic()
# Force XML output
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[
{"role": "user", "content": "List the top 3 risks of this project plan."},
{"role": "assistant", "content": "<risks>\n<risk>"}
]
)
# Force a specific classification label
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=10,
messages=[
{"role": "user", "content": (
"Classify this support ticket as one of: BILLING, TECHNICAL, GENERAL.\n\n"
"Ticket: I can\'t log into my account after the password reset."
)},
{"role": "assistant", "content": "Classification: "}
]
)Schema Validation
Even with prefilling, you should never trust that Claude's output perfectly matches your expected schema. Production systems must validate. The most robust approach uses a validation library like Pydantic.
import anthropic
import json
from pydantic import BaseModel, Field
from typing import List, Optional
class ExtractedEntity(BaseModel):
name: str
entity_type: str = Field(description="PERSON, ORG, LOCATION, DATE, PRODUCT")
confidence: float = Field(ge=0.0, le=1.0)
class ExtractionResult(BaseModel):
entities: List[ExtractedEntity]
summary: str
language: Optional[str] = None
def extract_entities(text: str) -> ExtractionResult:
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=[
{"role": "user", "content": (
f"Extract all named entities from the following text. "
f"Return a JSON object matching this schema:\n\n"
f"{ExtractionResult.model_json_schema()}\n\n"
f"<text>\n{text}\n</text>"
)},
{"role": "assistant", "content": "{"}
]
)
json_str = "{" + response.content[0].text
data = json.loads(json_str)
return ExtractionResult.model_validate(data)Retry Loops for Robustness
When JSON parsing or schema validation fails, a well-designed system retries with error feedback rather than crashing. The retry loop sends the validation error back to Claude so it can correct its output.
import anthropic
import json
from pydantic import BaseModel, ValidationError
from typing import List
import time
class AnalysisResult(BaseModel):
category: str
severity: str
findings: List[str]
recommendation: str
def extract_with_retry(
prompt: str,
max_retries: int = 3,
model: str = "claude-sonnet-4-20250514"
) -> AnalysisResult:
client = anthropic.Anthropic()
messages = [
{"role": "user", "content": prompt},
{"role": "assistant", "content": "{"}
]
for attempt in range(max_retries):
response = client.messages.create(
model=model,
max_tokens=2048,
messages=messages
)
json_str = "{" + response.content[0].text
try:
data = json.loads(json_str)
return AnalysisResult.model_validate(data)
except (json.JSONDecodeError, ValidationError) as e:
if attempt == max_retries - 1:
raise
# Feed the error back to Claude for correction
messages = [
{"role": "user", "content": prompt},
{"role": "assistant", "content": json_str},
{"role": "user", "content": (
f"The JSON you returned failed validation:\n"
f"{str(e)}\n\n"
f"Please return corrected JSON that matches the "
f"required schema. Return ONLY the JSON object."
)},
{"role": "assistant", "content": "{"}
]
raise RuntimeError("Should not reach here")Tool Use for Guaranteed Structure
Claude's tool use feature provides another path to structured output. When you define a tool with a JSON schema, Claude is constrained to produce output that matches that schema when it decides to use the tool. This is sometimes called “forced function calling” when combined with tool_choice.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[{
"name": "record_analysis",
"description": "Record the results of the text analysis",
"input_schema": {
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral", "mixed"]
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"topics": {
"type": "array",
"items": {"type": "string"}
},
"summary": {"type": "string"}
},
"required": ["sentiment", "confidence", "topics", "summary"]
}
}],
tool_choice={"type": "tool", "name": "record_analysis"},
messages=[{"role": "user", "content": (
"Analyze the following customer review:\n\n"
"<review>Great product, fast shipping, but packaging was damaged.</review>"
)}]
)
# Extract the structured tool input
tool_use = next(b for b in response.content if b.type == "tool_use")
result = tool_use.input
print(result) # Guaranteed to match the schema