Claude gives you structured JSON output every time with this one trick
← Back
April 4, 2026Claude6 min read

Claude gives you structured JSON output every time with this one trick

Published April 4, 20266 min read

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.

python
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

python
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

python
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

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

Share this
← All Posts6 min read