Back to all articles
13 MIN READ

Structured Outputs with Claude's Strict Mode

By Learnia AI Research Team

Structured Outputs with Claude's Strict Mode

📅 Last updated: March 10, 2026 — Covers strict: true, JSON schema validation, client/server tools, and tool search.

🔗 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.


How Claude's Tool Use Mechanism Works

Claude uses tools as its primary mechanism for producing structured outputs. Here's the flow:

Loading diagram…

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

AspectWithout strictWith strict: true
Required fieldsUsually presentAlways present
Correct typesAlmost alwaysAlways correct
Enum valuesSometimes off-listAlways within the list
Extra fieldsPossibleNever 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"}]
)
Loading diagram…

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

ValueBehavior
{"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 SearchWith Tool Search
200 definitions × ~200 tokens = 40K tokensLightweight description ~2K + 3 definitions = ~2.6K tokens
Imprecise selection (too many choices)Targeted, accurate selection
High per-request cost~90% cost reduction
Increased latencyOptimized 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

  1. Always use strict: true for structured output tools
  2. Always specify required for mandatory fields
  3. Use enum for constrained-choice values
  4. Force the tool with tool_choice when structured output is the main goal
  5. Write clear, concise descriptions — they guide Claude as much as the schema
  6. Test with edge case inputs (empty text, mixed languages, noisy data)

Pricing Considerations

Tool definitions consume input tokens. Here's how to optimize:

StrategyImpact
Concise descriptions-30% tokens per tool
Tool search for 50+ tools-80-90% input tokens
Forced tool_choiceAvoids an extra turn
Minimal schemasFewer tokens, same results
Reuse conversationsPrompt 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

ConceptKey Point
strict: trueGuarantees JSON schema compliance — zero parsing errors
Forced tool_choiceForces Claude to use a specific tool
Client toolsYou execute the tool — full control
Server toolsAPI executes (web_search, code_execution)
Tool searchReduces tokens by ~90% for 100+ tools
enum + requiredConstrain values and guarantee fields
Prompt cachingAutomatically 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.


Continue Your Learning

Newsletter

Weekly AI Insights

Tools, techniques & news — curated for AI practitioners. Free, no spam.

Free, no spam. Unsubscribe anytime.

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.