Skip to content

C3 · lesson 5 · 10 min read

JSON schemas and tool-forced output

Get machine-readable JSON with structured outputs, strict tools and tool_choice, and know which option each model accepts.

Ընտրեք տեքստը՝ հայերեն թարգմանությունը տեսնելու համար։Highlight text to see Armenian. Machine-assisted translation; English is the source of truth.

When a script, a pipeline or another agent consumes Claude’s answer, “usually valid JSON” is not good enough. One missing brace breaks the job at 3 a.m. The API gives you three tools for this, and they combine.

The options

Approach How What you get
Ask in the prompt Describe the JSON shape, show an example Usually right; you must parse and check
Structured outputs output_config.format with type: "json_schema" The text response is constrained to your schema
Strict tool use strict: true on a tool definition Tool input always matches its input_schema; name is valid
Forced tool tool_choice any or tool Claude must call a tool (on models that accept forcing)

Structured outputs and strict tools use grammar-constrained sampling: the model can only produce tokens that keep the output valid against the schema. They are generally available (no beta header), and you can use both in one request. The older output_format parameter is deprecated in favour of output_config.format.

Structured outputs with Pydantic

The Python SDK’s client.messages.parse() takes a Pydantic model, turns it into a schema, sends it as output_config.format, and returns the parsed object in parsed_output.

import os
import sys
from typing import Literal

import anthropic
from pydantic import BaseModel

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])


class Incident(BaseModel):
    title: str
    severity: Literal["sev1", "sev2", "sev3", "sev4"]
    services: list[str]
    started_at: str  # ISO 8601
    customer_impact: bool
    summary: str


response = client.messages.parse(
    model="claude-haiku-5-5",
    max_tokens=4096,
    system="You turn incident write-ups into records for the incident database.",
    messages=[{"role": "user", "content": f"<writeup>\n{sys.stdin.read()}\n</writeup>"}],
    output_format=Incident,
)
if response.stop_reason != "end_turn":
    sys.exit(f"No usable record: stop_reason={response.stop_reason}")
print(response.parsed_output.model_dump_json(indent=2))

Schema rules worth knowing: objects need additionalProperties: false (the SDK adds it), enum and const work, and so do common string formats such as date-time. Not supported: recursive schemas, numeric limits (minimum, maximum), string length limits, and most array limits. An unsupported feature returns a 400 error, so anything like “between 0 and 100” has to be checked in your code (lesson 6).

Forcing a tool

tool_choice decides whether Claude may, must or must not call your tools:

tool_choiceauto → text, a tool call, or bothany → must call one of the toolstool → must call the named toolnone → no tool calls, text only
The four tool_choice values.

A forced tool with a schema is a classic way to get JSON, and the trick is still useful for classification: a tool whose only input is an enum of valid labels.

TOOL = {
    "name": "record_alert_class",
    "description": "Record the triage class for one monitoring alert.",
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "label": {"type": "string", "enum": ["page", "ticket", "ignore"]},
            "explanation": {"type": "string"},
        },
        "required": ["label", "explanation"],
        "additionalProperties": False,
    },
}

response = client.messages.create(
    model="claude-haiku-5-5",
    max_tokens=1024,
    tools=[TOOL],
    tool_choice={"type": "tool", "name": "record_alert_class"},
    messages=[{"role": "user", "content": "<alert>Disk /var 97% on node-12</alert>"}],
)
call = next(b for b in response.content if b.type == "tool_use")
print(call.input["label"], "-", call.input["explanation"])

With any or tool, the API starts the assistant turn for you, so there is no text before the tool_use block even if you ask for one, and thinking is skipped. If you want an explanation or thinking first, use auto and tell Claude in the prompt to call the tool.

What replaced prefill

Starting the assistant turn with { used to be the cheap way to force JSON. Current models reject a prefilled final assistant turn, and prefill was never compatible with structured outputs anyway. Use structured outputs for shapes and an enum tool or schema for labels. Structured outputs also can’t be combined with citations.

Recap

  • For machine-readable JSON, use structured outputs (output_config.format) or strict tools, not prompt-only JSON.
  • In Python, messages.parse() with a Pydantic model returns parsed_output.
  • The schema subset excludes numeric and length limits and recursion; check those in code.
  • tool_choice is auto, any, tool or none; forcing skips any text and thinking before the call.
  • Opus 5.5, Sonnet 5.5, Fable 5.1 and Mythos 5.1 reject forced tools; use auto plus strict tools or structured outputs.

$ ./try-it.sh

Try it: extract an incident record as validated JSON

  1. Install the SDK and Pydantic (pip install anthropic pydantic) and export ANTHROPIC_API_KEY.
  2. Save the first script from this lesson as extract.py and run it on an incident write-up: python extract.py < incident.md.
  3. Change severity to a free str in the model, run it on several incidents, and compare how consistent the values are.
  4. Run the forced-tool variant on claude-haiku-5-5, then switch the model to claude-opus-5-5 and read the 400 error it returns for tool_choice tool.

Check yourself

4 questions. Original practice questions, not taken from the real exam.

  1. A deployment pipeline parses Claude's text answer as JSON, and about one run in fifty fails on malformed output.

    1. Which request parameter constrains the text response to a JSON schema?

  2. Code that worked on claude-opus-5 sends tool_choice {"type": "tool", "name": "record_incident"}. After switching to claude-opus-5-5, every request returns a 400 error.

    2. What is the recommended fix?

  3. You're writing a JSON schema for structured outputs that describes a scaling recommendation for an Auto Scaling group.

    3. Which two schema features are not supported and cause a 400 error? (Choose two.)

    Choose 2.

  4. On a model that accepts forced tools, you set tool_choice {"type": "any"} and ask in the user message for a one-line explanation before the tool call, for the audit log. The explanation never appears.

    4. Why is the explanation missing?

Answer every question to check.