Tools

Make Claude return schema-valid JSON: use tool use instead of “please return JSON”

TL;DR

Declare a tool with an input_schema (JSON Schema), set tool_choice to force that tool, then read the input of the tool_use block. Validate server-side and retry on failure. The Claude API also has an official structured outputs feature — check the docs to pick the right approach.

“Return JSON only, no explanation” — that prompt works 95% of the time. The other 5% is when the model prefixes the JSON with “Here’s the result:”, writes "High" instead of "high", or drops a required field… and your parser crashes at 2 a.m. With Claude there are two far more reliable options: structured outputs (constraining the output to a JSON Schema) and tool use with strict schemas.

Three ways to get JSON from Claude

MethodReliabilityUse when
Prompt “return JSON”Low — down to luckQuick prototypes, never production
JSON outputs (output_config.format)High — constrained decoding follows the schemaThe final answer must be parseable: extraction, classification, reports
Strict tool use (strict: true)High — tool names and parameters match the schemaAgents calling your functions, where bad parameters break things

The last two can be used separately or together in one request — for instance an agent that calls tools precisely and also returns a structured final answer.

Option 1: JSON outputs

Declare a schema in output_config.format. Claude is constrained while generating tokens, so the output matches the schema (with the exceptions below) and you don’t need format-retry loops:

import json, anthropic
client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=500,
    messages=[{"role": "user", "content": ticket_text}],
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["bug", "billing", "feature", "other"]},
                    "priority": {"type": "string", "enum": ["P1", "P2", "P3"]},
                    "summary":  {"type": "string"}
                },
                "required": ["category", "priority", "summary"],
                "additionalProperties": False
            }
        }
    },
)
data = json.loads(next(b.text for b in resp.content if b.type == "text"))

Cleaner with Pydantic (Python SDK)

from pydantic import BaseModel
from typing import Literal

class Ticket(BaseModel):
    category: Literal["bug", "billing", "feature", "other"]
    priority: Literal["P1", "P2", "P3"]
    summary: str

resp = client.messages.parse(
    model="claude-sonnet-5-5", max_tokens=500,
    messages=[{"role": "user", "content": ticket_text}],
    output_format=Ticket,
)
ticket = resp.parsed_output   # a validated Ticket instance

The SDK converts the Pydantic model into a schema, sends it as output_config.format and validates the result. Constraints the API doesn’t support (such as minimum or maxLength) are moved into field descriptions by the SDK and still checked in your code.

Option 2: Strict tool use

When an agent calls your real functions (booking, creating tickets, querying a database), add "strict": true to the tool definition so tool names and parameters always match the schema:

tools = [{
    "name": "save_ticket",
    "description": "Save a classified ticket",
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "category": {"type": "string", "enum": ["bug", "billing", "feature", "other"]},
            "priority": {"type": "string", "enum": ["P1", "P2", "P3"]},
            "summary":  {"type": "string"}
        },
        "required": ["category", "priority", "summary"],
        "additionalProperties": False
    }
}]

msg = client.messages.create(
    model="claude-sonnet-5-5", max_tokens=500, tools=tools,
    tool_choice={"type": "tool", "name": "save_ticket"},   # force this tool
    messages=[{"role": "user", "content": ticket_text}])

data = next(b.input for b in msg.content if b.type == "tool_use")

The old trick of forcing a tool with tool_choice and reading its input still helps; adding strict moves you from “usually right” to “guaranteed to match the schema”.

JSON Schema limits to know

SupportedNot supported (400 error)
object, array, string, integer, number, boolean, nullRecursive schemas
enum, const, anyOf, allOfminimum, maximum, multipleOf
Internal $ref, default, requiredminLength, maxLength
Formats: date-time, date, email, uri, uuid, ipv4…additionalProperties other than false
Simple pattern regexesRegexes with backreferences or lookaround

There are complexity limits too: up to 20 strict tools per request, 24 optional parameters and 16 union-type parameters across all strict schemas. Overly complex schemas fail with a “too complex for compilation” error — flatten structures, make parameters required, or mark only critical tools as strict.

When output can still deviate

  • stop_reason: "refusal" — the refusal message takes precedence, so the output may not match the schema.
  • stop_reason: "max_tokens" — output is truncated mid-way and invalid. Raise max_tokens and retry.
  • Enum casing — capitalisation can occasionally differ from the schema without an error. Compare case-insensitively and avoid enum values that differ only by case.

Performance and cost

  • The first request with a new schema is slower because the grammar is compiled; compiled grammars are cached for 24 hours from last use.
  • Changing the schema structure invalidates the cache; changing only name or description doesn’t.
  • The feature injects a short system prompt describing the format → a few extra input tokens.
  • It can’t be combined with citations (400 error) or answer prefilling; it works fine with the Batch API (50% off), streaming and token counting.

Production checklist

  1. Use JSON outputs or strict tool use instead of prompt instructions alone.
  2. Still validate on the server (Pydantic / Zod) — that catches refusals and max_tokens cases.
  3. Check stop_reason before parsing.
  4. Log input/output of failures to improve prompts and schemas.
  5. Simple extraction → small models; bulk runs → combine with the Batch API.
  6. Never put sensitive data (e.g. health information) in field names, enum values or patterns — schemas are cached; keep real data in message content.

How to apply it

  • Developers: Find every place that json.loads()s output from a “return JSON” prompt and switch to messages.parse() with Pydantic. Delete the format-retry loops while you’re there.
  • Tech Leads: Standardise: every tool that calls a system with side effects uses strict and gets a schema review, like an API contract.
  • PMs: When the team estimates an “AI data extraction” feature, the parsing and retry work is now much smaller — put the time into schema design and evaluation tests for content quality.

Sources & references

  1. Structured outputs — Claude Docs