Structured Outputs with Claude's Strict Mode
By Dorian Laurenceau
📅 Last reviewed: April 24, 2026. Updated with April 2026 findings and community feedback.
🔗 Related articles: Claude Tool Use Guide · Claude API Complete Guide · Reliable JSON Output from LLMs
In production, an LLM that returns free-form text is a liability. One missing field, one incorrect type, one renamed key, and your pipeline breaks. Claude's strict mode solves this: it guarantees that every output matches your JSON schema exactly.
Why Structured Outputs Matter in Production
When you integrate Claude into an application, you need predictable data:
- →APIs: endpoints expect precise formats
- →Databases: inserts require exact types
- →Pipelines: each stage consumes the output of the previous one
- →UI: front-end components expect defined structures
Without guaranteed structure, you're writing validation code, handling error cases, and hoping the model doesn't change format between calls.
For a broader perspective on why structured outputs are critical in AI systems, see our detailed article on why structured AI outputs matter.
The honest read on strict-mode structured outputs, from engineers who've shipped them in production on r/LocalLLaMA, r/MachineLearning, and r/LangChain: strict mode eliminates the schema-violation class of failure, which used to account for 30-50% of production LLM incidents. It does not eliminate the semantic-correctness class, which is now the dominant failure mode. Your JSON will parse; it may still contain the wrong values. The Anthropic tool use documentation and the OpenAI structured outputs post-mortem both acknowledge this trade-off.
Where the community correctly pushes back on "JSON mode solves reliability" claims: the constraint mechanism (grammar-constrained decoding) has measurable quality costs on complex schemas — the constrained decoding papers show that aggressive constraints can degrade reasoning on nested structures and long outputs. Teams that blindly wrap every call in strict mode sometimes reduce output quality for the privilege of never handling a parse error.
Pragmatic rule from shipping teams: use strict mode at the boundaries — the exact places where your code calls JSON.parse and would crash on a malformed response. Don't use it in the middle of a chain-of-thought or where the model needs latitude to reason. Pair it with a semantic validator (pydantic, zod, or a custom check) because a well-formed JSON with a hallucinated foreign key is a much nastier bug than a parse error you can retry on.
How Claude's Tool Use Mechanism Works
Claude uses tools as its primary mechanism for producing structured outputs. Here's the flow:
When you define a tool with a JSON schema, Claude generates the parameters in the exact format you specified. This is the mechanism that strict mode reinforces.
Defining a Basic Tool
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "extract_contact",
"description": "Extract contact information from text",
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Full name of the person"
},
"email": {
"type": "string",
"description": "Email address"
},
"phone": {
"type": "string",
"description": "Phone number"
}
},
"required": ["name", "email"]
}
}
]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=[{
"role": "user",
"content": "Contact Marie Dupont at marie@example.com or 555-0123"
}]
)
The strict: true Parameter
Strict mode is enabled by adding strict: true at the tool definition level. This forces Claude to produce JSON that passes schema validation, on every call, without exception.
tools = [
{
"name": "analyze_sentiment",
"description": "Analyze the sentiment of a text",
"strict": True, # ← Enables strict mode
"input_schema": {
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"]
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"keywords": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["sentiment", "confidence", "keywords"]
}
}
]
What strict: true Guarantees
| Aspect | Without strict | With strict: true |
|---|---|---|
| Required fields | Usually present | Always present |
| Correct types | Almost always | Always correct |
| Enum values | Sometimes off-list | Always within the list |
| Extra fields | Possible | Never added |
| Reliable parsing | ~95-99% | 100% error-free |
Advanced JSON Schema Patterns
Nested Objects
For complex structures, nest objects within your schema:
tools = [
{
"name": "extract_invoice",
"description": "Extract data from an invoice",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"invoice_number": {"type": "string"},
"date": {"type": "string", "description": "ISO 8601 format"},
"vendor": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {"type": "string"},
"tax_id": {"type": "string"}
},
"required": ["name", "address"]
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "integer"},
"unit_price": {"type": "number"},
"total": {"type": "number"}
},
"required": ["description", "quantity", "unit_price", "total"]
}
},
"total_amount": {"type": "number"},
"currency": {
"type": "string",
"enum": ["EUR", "USD", "GBP"]
}
},
"required": ["invoice_number", "date", "vendor", "line_items", "total_amount", "currency"]
}
}
]
Enums and Constrained Categories
Enums are especially powerful with strict mode, Claude will never produce a value outside the list:
"priority": {
"type": "string",
"enum": ["critical", "high", "medium", "low"],
"description": "Priority level for the ticket"
}
Typed Arrays
For lists of elements with guaranteed structure:
"tags": {
"type": "array",
"items": {
"type": "string",
"enum": ["bug", "feature", "improvement", "documentation"]
},
"description": "Tags applicable to the issue"
}
Client Tools vs Server Tools
Claude supports two types of tools, and both can use strict mode:
Client Tool Workflow
This is the most common pattern for structured outputs in production:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": user_input}]
)
# Extract the structured result
for block in response.content:
if block.type == "tool_use":
# block.input contains the schema-validated JSON
structured_data = block.input
print(f"Tool: {block.name}")
print(f"Data: {structured_data}")
# No need for try/except for JSON parsing
# In strict mode, the structure is guaranteed
process_data(structured_data)
Server Tool Workflow (Built-in)
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
tools=[{
"type": "web_search_20250305",
"name": "web_search",
"max_uses": 3
}],
messages=[{"role": "user", "content": "Latest news about Claude"}]
)
Programmatic Tool Calling (Forcing Usage)
By default, Claude decides whether to use a tool. For structured outputs, you often want to force the call:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "extract_contact"}, # Force the tool
messages=[{"role": "user", "content": text}]
)
tool_choice Options
| Value | Behavior |
|---|---|
{"type": "auto"} | Claude decides whether to use a tool (default) |
{"type": "any"} | Claude must use at least one tool |
{"type": "tool", "name": "X"} | Claude must use tool X specifically |
{"type": "none"} | Claude cannot use any tools |
💡 Production tip: Use
tool_choice: {"type": "tool", "name": "..."}when you're using a tool as a structured output mechanism, not as a function to execute. This is the most reliable pattern.
Tool Search: Handling Hundreds of Tools
When your system offers over 100 tools, including them all in every request is expensive in tokens and reduces selection accuracy. Tool search solves this problem.
The Concept
Instead of sending all tool definitions, you send a text description of each tool. Claude selects the relevant ones, then you resend only the full definitions for those.
# Step 1: Lightweight descriptions of all tools
tool_descriptions = """
Available tools:
- get_user_profile: Retrieve a user's profile by ID
- update_user_email: Update a user's email address
- list_invoices: List a client's invoices
- create_invoice: Create a new invoice
- send_notification: Send a push notification
- get_analytics: Retrieve usage metrics
... (200+ tools)
"""
# Step 2: Claude selects the relevant tools
selection_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
messages=[{
"role": "user",
"content": f"User request: '{user_query}'\n\n{tool_descriptions}\n\nWhich tools are needed? Reply with names only."
}]
)
# Step 3: Call with only the selected tools
selected_tools = parse_tool_names(selection_response)
full_tool_defs = [t for t in all_tools if t["name"] in selected_tools]
final_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
tools=full_tool_defs, # Only the relevant tools
messages=[{"role": "user", "content": user_query}]
)
Tool Search Benefits
| Without Tool Search | With Tool Search |
|---|---|
| 200 definitions × ~200 tokens = 40K tokens | Lightweight description ~2K + 3 definitions = ~2.6K tokens |
| Imprecise selection (too many choices) | Targeted, accurate selection |
| High per-request cost | ~90% cost reduction |
| Increased latency | Optimized latency |
To see how this pattern fits into complex agent architectures, check our guide on Claude agent architecture patterns.
Error Handling and Best Practices
Extracting Results Cleanly
def extract_tool_result(response):
"""Extract the structured result from a Claude tool response."""
for block in response.content:
if block.type == "tool_use":
return {
"tool_name": block.name,
"tool_id": block.id,
"data": block.input # JSON guaranteed valid in strict mode
}
return None
# Usage
result = extract_tool_result(response)
if result:
# In strict mode, every field is guaranteed present and typed
contact = result["data"]
save_to_database(contact["name"], contact["email"])
Multi-turn Pattern (Conversation with Tools)
messages = [{"role": "user", "content": user_query}]
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
tools=tools,
messages=messages
)
# Check if Claude wants to use a tool
if response.stop_reason == "tool_use":
# Execute each requested tool
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
})
# Append Claude's response and the tool results
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
else:
# Final response (no tool use)
break
Production Checklist
- →Always use
strict: truefor structured output tools - →Always specify
requiredfor mandatory fields - →Use
enumfor constrained-choice values - →Force the tool with
tool_choicewhen structured output is the main goal - →Write clear, concise descriptions, they guide Claude as much as the schema
- →Test with edge case inputs (empty text, mixed languages, noisy data)
Pricing Considerations
Tool definitions consume input tokens. Here's how to optimize:
| Strategy | Impact |
|---|---|
| Concise descriptions | -30% tokens per tool |
| Tool search for 50+ tools | -80-90% input tokens |
Forced tool_choice | Avoids an extra turn |
| Minimal schemas | Fewer tokens, same results |
| Reuse conversations | Prompt caching (automatic reduction) |
💡 Tool definitions benefit from Anthropic's prompt caching. If you send the same tools on every request, the token cost is automatically reduced after the first call.
For more details on the Claude API and pricing options, see the Claude API Complete Guide.
Full Example: Data Extraction Pipeline
Here's a complete production example combining all the concepts:
import anthropic
import json
client = anthropic.Anthropic()
# Tool definition with strict mode
extraction_tool = {
"name": "extract_product_review",
"description": "Extract structured data from a product review",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"product_name": {"type": "string"},
"rating": {
"type": "integer",
"description": "Rating from 1 to 5"
},
"sentiment": {
"type": "string",
"enum": ["very_positive", "positive", "neutral", "negative", "very_negative"]
},
"pros": {
"type": "array",
"items": {"type": "string"},
"description": "Positive points mentioned"
},
"cons": {
"type": "array",
"items": {"type": "string"},
"description": "Negative points mentioned"
},
"recommendation": {"type": "boolean"},
"summary": {
"type": "string",
"description": "One-sentence summary of the review"
}
},
"required": [
"product_name", "rating", "sentiment",
"pros", "cons", "recommendation", "summary"
]
}
}
def analyze_review(review_text: str) -> dict:
"""Analyze a product review and return structured data."""
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[extraction_tool],
tool_choice={"type": "tool", "name": "extract_product_review"},
messages=[{
"role": "user",
"content": f"Analyze this product review:\n\n{review_text}"
}]
)
for block in response.content:
if block.type == "tool_use":
return block.input # Structure guaranteed by strict: true
return None
# Usage
review = """
I bought the Sony WH-1000XM5 headphones 3 months ago. The noise cancellation
is exceptional, the comfort is top-notch, and the battery easily lasts
30 hours. Only downside: the price is a bit high and the carrying case
is less compact than the XM4. Despite that, I recommend them 100%.
"""
result = analyze_review(review)
print(json.dumps(result, indent=2))
# {
# "product_name": "Sony WH-1000XM5",
# "rating": 4,
# "sentiment": "very_positive",
# "pros": ["Exceptional noise cancellation", "Top-notch comfort", "30h battery life"],
# "cons": ["High price", "Less compact case than XM4"],
# "recommendation": true,
# "summary": "Excellent headphones with outstanding noise cancellation, despite a premium price."
# }
Summary
| Concept | Key Point |
|---|---|
strict: true | Guarantees JSON schema compliance, zero parsing errors |
Forced tool_choice | Forces Claude to use a specific tool |
| Client tools | You execute the tool, full control |
| Server tools | API executes (web_search, code_execution) |
| Tool search | Reduces tokens by ~90% for 100+ tools |
enum + required | Constrain values and guarantee fields |
| Prompt caching | Automatically reduces cost for repeated tool definitions |
Structured outputs with strict mode transform Claude from a text generator into a reliable data engine. Combined with agent architecture patterns and a robust JSON pipeline, you have everything you need to build solid production systems.
- →Reliable JSON Output from LLMs, Complementary techniques
- →Why Structured AI Outputs Matter, Context and motivations
- →Claude Agent Architecture Patterns, Patterns for complex systems
- →Claude API Complete Guide, Full API reference
Dorian Laurenceau
Full-Stack Developer & Learning DesignerFull-stack web developer and learning designer. I spent 4 years as a freelance full-stack developer and 4 years teaching React, JavaScript, HTML/CSS and WordPress to adult learners. Today I design learning paths in web development and AI, grounded in learning science. I founded learn-prompting.fr to make AI practical and accessible, and built the Bluff app to gamify political transparency.
Weekly AI Insights
Tools, techniques & news — curated for AI practitioners. Free, no spam.
Free, no spam. Unsubscribe anytime.
→Related Articles
FAQ
What is strict mode in Claude tools?+
Strict mode (strict: true) forces Claude to produce JSON outputs that exactly match the defined tool schema. Every field, type, and constraint is guaranteed, eliminating parsing errors in production.
What's the difference between client tools and server tools?+
A client tool returns the result to your code for local execution. A server tool is executed directly by the Anthropic API (like web_search or code_execution). Both support strict mode.
How do I handle 100+ tools with Claude?+
Use tool search: send a text description of all your tools and let Claude select the most relevant ones. This reduces token consumption and improves call accuracy.
Does strict mode affect pricing?+
Strict mode itself doesn't add direct cost. However, tool definitions consume input tokens. Optimize by using concise descriptions and tool search for large tool sets.