How much each technique guarantees
- “Return JSON” in the promptusually works; can break or drift
- Plus examplesmore consistent shape — still not enforced
- Tool use with a schemaoutput arrives as a typed
tool_useinput - Strict mode / JSON outputsconstrained decoding: matches unless refused or cut off
- Your validationare the values right? — see 4.4
Asking versus enforcing
A prompt that says “respond only with JSON in this format” works most of the time, which is exactly the problem at scale. Across a hundred thousand documents you will see a friendly sentence before the opening brace, a trailing comment, a missing field, a number sent as the string "2", or a key spelled slightly differently. Each one is a crash or a silent data error downstream. Anthropic’s consistency guide is direct about this: for guaranteed schema conformance, use structured outputs rather than prompt engineering.
The oldest reliable pattern — and the one this task statement names — is to use tool use as a structuring device. You define a “tool” whose input_schema is the shape you want, then read the arguments Claude supplies. Nothing is ever executed: the tool call is the output. Because tool inputs are generated against a schema, this is far more reliable than free text, and it hands your code a parsed object rather than a string to parse.
record_invoice = {
"name": "record_invoice",
"description": "Record the fields extracted from one supplier invoice.",
"strict": True, # constrained to the schema below
"input_schema": {
"type": "object",
"properties": {
"supplier": {"type": "string"},
"invoice_date": {"type": "string", "description": "YYYY-MM-DD"},
"po_number": {"type": ["string", "null"]}, # null if not printed
"currency": {"type": "string", "enum": ["GBP", "EUR", "USD", "other"]},
"total": {"type": "number"},
},
"required": ["supplier", "invoice_date", "po_number", "currency", "total"],
"additionalProperties": False,
},
}
response = client.messages.create(
model=MODEL, max_tokens=1024,
tools=[record_invoice],
tool_choice={"type": "tool", "name": "record_invoice"}, # must call it
messages=[{"role": "user", "content": invoice_text}],
)
data = next(b.input for b in response.content if b.type == "tool_use")Structured extraction, message by message
record_invoice schematool_choice: call record_invoicetool_use with typed inputtool_use block’s input is the structured result.The enforcement options today
Current documentation groups two features under the name structured outputs, and both use constrained decoding: the schema is compiled into a grammar that restricts which tokens can be generated, so — refusals and truncation aside — a non-conforming response cannot be produced. Strict tool use ("strict": true on a tool) guarantees that tool inputs follow the input_schema and that the tool name is valid. JSON outputs (output_config.format with type: "json_schema") constrain Claude’s ordinary response to a schema when you do not need a tool at all. The docs sum up the split neatly: JSON outputs control what Claude says; strict tool use controls how it calls your functions.
| Technique | What it guarantees | Use when |
|---|---|---|
| Prompt says “return JSON” | Nothing — best effort | Prototypes only |
Tool with input_schema | Output arrives as a structured tool input; shape strongly guided | Structured extraction, especially with several possible schemas |
Tool with strict: true | Inputs match the schema; name is valid | An agent’s tools, or extraction through a tool, in production |
output_config.format (JSON outputs) | The response text is valid JSON matching the schema | You want a JSON answer, not a tool call |
Agent SDK output_format / outputFormat | Validated JSON at the end of a multi-turn agent run; re-prompts on mismatch | An agent uses tools, then must return a typed result |
response = client.messages.create(
model=MODEL, max_tokens=1024,
messages=[{"role": "user", "content": invoice_text}],
output_config={"format": {"type": "json_schema", "schema": INVOICE_SCHEMA}},
)
data = json.loads(response.content[0].text) # guaranteed to parseMaking sure a tool is called: tool_choice
A tool-based extractor only works if Claude calls the tool rather than replying in prose. That is controlled by tool_choice, which has four values. auto (the default when tools are provided) lets Claude decide. any means it must call one of your tools but may pick which. tool forces one named tool. none prevents tool use. With any or tool, the API prefills the assistant turn so the response starts with the tool call — Claude will not write explanatory text first, even if asked to.
Choosing tool_choice for extraction
- One schema, always
tool— force ite.g.record_invoice - Several schemas, type unknown
any— must pick oneinvoice, receipt or contract - Tool may not be needed
autoClaude may answer in text - Want prose, no tools
none
The any case deserves attention. If a mailroom pipeline receives invoices, receipts and contracts, you can define one extraction tool per document type and set tool_choice to any. Claude must return structured data, and choosing the right tool is itself the classification step. The docs also recommend combining any with strict tools to guarantee both that some tool is called and that its inputs follow the schema.
Designing a schema that allows honest answers
Enforcement cuts both ways. If a field is required and typed as a string, the model must put a string there — even when the document does not contain one. A strictly enforced schema with no way to say “not present” is an instruction to invent. Good extraction schemas give the model honest exits.
A schema that forces guessing versus one that allows honesty
Forces fabrication
po_number: requiredstringcategory: enum with no “other”date: free text, format unstated- No way to flag an unreadable page
Allows honest answers
po_number:stringornull- Enum with
otherplus acategory_detailstring datedescribed asYYYY-MM-DD- An
unclearvalue orneeds_reviewflag
- Nullable over optional-and-guessed. Make fields that may be absent nullable (or not required), and say in the prompt or an example that
nullmeans “not in the document”. - Enums with an escape hatch. A closed list forces a nearest-fit label. Add
otherand a free-text detail field so new categories are captured instead of mis-filed. - Describe formats. Put date, currency and unit conventions in property descriptions. Where the JSON Schema feature is unsupported, the SDKs move it into the description for you.
- Close the object.
"additionalProperties": falsestops invented keys; structured outputs recommend it on every object. - Keep it focused. The Agent SDK docs warn that deeply nested schemas with many required fields are harder to satisfy; the API also enforces complexity limits on strict schemas.
What enforcement never guarantees
Even with strict mode, three cases still reach your code with non-conforming or incomplete output, and the docs call out the first two explicitly. A refusal (stop_reason: "refusal") takes precedence over the schema. A response that hits max_tokens is cut off and may not match — retry with a higher limit. And a response that matches perfectly can still be wrong: the right type in the wrong field, a total that does not add up, a date from the letterhead instead of the invoice line. Check stop_reason before trusting the payload, then validate meaning in code — which is where 4.4 takes over.
Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Asking for JSON in the prompt and parsing free text in production | Enforce a schema with a tool (strict: true) or JSON outputs. |
| Treating schema-valid output as correct output | Validate the values in code — totals, dates, cross-field rules. |
| Required non-null fields for data that may be missing | Make them nullable and show that null means “not in the document”. |
| A closed enum with no fallback | Add other plus a detail field, or an unclear value. |
Forcing any with no tool for “none of these” | Add a tool that lets the model flag an unrecognised input. |
You should now be able to
- Use a tool’s
input_schemaas an extraction schema and read the result from thetool_useblock. - Choose
auto,any,toolornonefortool_choicegiven the scenario. - Explain what strict tool use and JSON outputs guarantee — and what they do not.
- Design schemas with nullable fields, enum fallbacks and described formats to prevent fabrication.
- Handle
refusalandmax_tokensbefore trusting a structured payload.