Claude gives you structured JSON output every time with this one trick
I spent months parsing Claude's text responses with regex and custom parsers, chasing edge cases where the format drifted slightly. Then I found the forced-tool-call trick — define a tool with your exact output schema, instruct Claude to always call it, and you get perfectly structured JSON every single time. No parsing. No regex. No edge cases.
The problem with "return JSON" prompts
The naive approach is to add to your system prompt: "Always respond with valid JSON in this format: {...}". This works 90% of the time. The other 10%, Claude adds a preamble ("Here is the JSON you requested:"), wraps the JSON in markdown code fences, or subtly changes a field name. In production, that 10% is unacceptable.
The tool-forcing pattern
Claude's tool use has a feature most engineers overlook: you can specify tool_choice to force Claude to always call a specific tool. Combined with a tool whose schema exactly matches your desired output, this gives you guaranteed structured output.
import anthropic
import json
client = anthropic.Anthropic()
def extract_structured(
text: str,
schema: dict,
tool_name: str = "extract",
system: str = "Extract the requested information from the text.",
) -> dict:
"""
Force Claude to return structured output matching `schema`.
Uses tool_choice to guarantee the tool is always called.
"""
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system=system,
tools=[
{
"name": tool_name,
"description": "Extract and return the structured information",
"input_schema": schema,
}
],
tool_choice={"type": "tool", "name": tool_name}, # Force this tool
messages=[{"role": "user", "content": text}],
)
# The response WILL have a tool_use block — guaranteed by tool_choice
for block in response.content:
if block.type == "tool_use":
return block.input
raise RuntimeError("Unexpected: no tool_use block in response")
Real-world example: sentiment analysis
sentiment_schema = {
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral", "mixed"],
"description": "Overall sentiment of the text",
},
"confidence": {
"type": "number",
"description": "Confidence score between 0 and 1",
},
"key_phrases": {
"type": "array",
"items": {"type": "string"},
"description": "Phrases that most influenced the sentiment",
},
"summary": {
"type": "string",
"description": "One sentence explanation of the sentiment",
},
},
"required": ["sentiment", "confidence", "key_phrases", "summary"],
}
review = """
The laptop is fantastic — blazing fast, great keyboard, beautiful display.
But the battery only lasts 4 hours and the fan noise is distracting.
"""
result = extract_structured(review, sentiment_schema)
print(result)
# {
# "sentiment": "mixed",
# "confidence": 0.92,
# "key_phrases": ["blazing fast", "great keyboard", "battery only lasts 4 hours", "fan noise"],
# "summary": "Strong positives on performance and design offset by significant battery and noise issues."
# }
Example: entity extraction
entity_schema = {
"type": "object",
"properties": {
"people": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"role": {"type": "string"},
},
"required": ["name"],
},
},
"organizations": {"type": "array", "items": {"type": "string"}},
"locations": {"type": "array", "items": {"type": "string"}},
"dates": {"type": "array", "items": {"type": "string"}},
},
"required": ["people", "organizations", "locations", "dates"],
}
news_text = """
Apple CEO Tim Cook announced yesterday that the company plans to open
a new research center in Bangalore by Q3 2026, partnering with IIT Bombay.
"""
entities = extract_structured(
news_text,
entity_schema,
system="Extract all named entities from the text.",
)
# {
# "people": [{"name": "Tim Cook", "role": "CEO"}],
# "organizations": ["Apple", "IIT Bombay"],
# "locations": ["Bangalore"],
# "dates": ["yesterday", "Q3 2026"]
# }
TypeScript version
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
async function extractStructured(
text: string,
schema: Record,
system = 'Extract the requested information.',
): Promise {
const response = await client.messages.create({
model: 'claude-opus-4-5',
max_tokens: 1024,
system,
tools: [
{
name: 'extract',
description: 'Return the extracted structured data',
input_schema: schema as Anthropic.Tool['input_schema'],
},
],
tool_choice: { type: 'tool', name: 'extract' },
messages: [{ role: 'user', content: text }],
});
for (const block of response.content) {
if (block.type === 'tool_use') {
return block.input as T;
}
}
throw new Error('No tool_use block in response');
}
// Usage with TypeScript types
interface ProductInfo {
name: string;
price: number;
currency: string;
inStock: boolean;
}
const info = await extractStructured(
'The Sony WH-1000XM5 headphones are available for $349 USD. Currently in stock.',
{
type: 'object',
properties: {
name: { type: 'string' },
price: { type: 'number' },
currency: { type: 'string' },
inStock: { type: 'boolean' },
},
required: ['name', 'price', 'currency', 'inStock'],
},
);
When this pattern is essential
Use forced tool calls whenever:
- You are piping Claude output directly into another system (no human review)
- You need to guarantee field names and types match a TypeScript interface
- You are processing bulk documents and cannot handle format variations
- You are storing Claude output in a database with a strict schema
For conversational use where you just want Claude to respond in a certain style, prompt-based instructions are fine. But for any automated pipeline, use this pattern. I have not had a parsing error in any project since switching to it.