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.
Հայերեն
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:
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 returnsparsed_output. - The schema subset excludes numeric and length limits and recursion; check those in code.
tool_choiceisauto,any,toolornone; 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
autoplus strict tools or structured outputs.
$ ./try-it.sh
Try it: extract an incident record as validated JSON
- Install the SDK and Pydantic (
pip install anthropic pydantic) and exportANTHROPIC_API_KEY. - Save the first script from this lesson as
extract.pyand run it on an incident write-up:python extract.py < incident.md. - Change
severityto a freestrin the model, run it on several incidents, and compare how consistent the values are. - Run the forced-tool variant on
claude-haiku-5-5, then switch the model toclaude-opus-5-5and read the 400 error it returns fortool_choicetool.
Check yourself
4 questions. Original practice questions, not taken from the real exam.