Rubric
Contents — domains, guide and mocks

Structured output with schemas

CCAR-F 4.316 min read · checked 21 September 2026

Task statementEnforce structured output using tool use and JSON schemas

How much each technique guarantees

  1. “Return JSON” in the promptusually works; can break or drift
  2. Plus examplesmore consistent shape — still not enforced
  3. Tool use with a schemaoutput arrives as a typed tool_use input
  4. Strict mode / JSON outputsconstrained decoding: matches unless refused or cut off
  5. Your validationare the values right? — see 4.4
Each layer adds a stronger guarantee. The last one is always yours: no schema can tell whether a value is true.

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.

Extraction through a tool — the tool is never runpython
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

Your code
Claude API
Downstream
Step 1: Your code to Claude API: Document + record_invoice schema
Step 2: Your code to Claude API: tool_choice: call record_invoice
Step 3: Claude API to Your code: tool_use with typed input
Step 4: Your code : Check values (totals, dates)
Step 5: Your code to Downstream: Write the record
There is no second request and no tool execution. The tool_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.

TechniqueWhat it guaranteesUse when
Prompt says “return JSON”Nothing — best effortPrototypes only
Tool with input_schemaOutput arrives as a structured tool input; shape strongly guidedStructured extraction, especially with several possible schemas
Tool with strict: trueInputs match the schema; name is validAn agent’s tools, or extraction through a tool, in production
output_config.format (JSON outputs)The response text is valid JSON matching the schemaYou want a JSON answer, not a tool call
Agent SDK output_format / outputFormatValidated JSON at the end of a multi-turn agent run; re-prompts on mismatchAn agent uses tools, then must return a typed result
The same schema as a JSON output, no tool involvedpython
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 parse

Making 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

What must happen on this request?
  • 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: required string
  • category: enum with no “other”
  • date: free text, format unstated
  • No way to flag an unreadable page

Allows honest answers

  • po_number: string or null
  • Enum with other plus a category_detail string
  • date described as YYYY-MM-DD
  • An unclear value or needs_review flag
  • Nullable over optional-and-guessed. Make fields that may be absent nullable (or not required), and say in the prompt or an example that null means “not in the document”.
  • Enums with an escape hatch. A closed list forces a nearest-fit label. Add other and 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": false stops 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 wrongDo this instead
Asking for JSON in the prompt and parsing free text in productionEnforce a schema with a tool (strict: true) or JSON outputs.
Treating schema-valid output as correct outputValidate the values in code — totals, dates, cross-field rules.
Required non-null fields for data that may be missingMake them nullable and show that null means “not in the document”.
A closed enum with no fallbackAdd 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_schema as an extraction schema and read the result from the tool_use block.
  • Choose auto, any, tool or none for tool_choice given 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 refusal and max_tokens before trusting a structured payload.

Practice questions

Original questions written for this lesson, in the exam’s style. Answer first, then open the reasoning — every option is explained, including why the wrong ones are tempting.

  1. Question 1

    A claims pipeline asks Claude to “respond only with valid JSON” describing each claim. Roughly one response in fifty has a sentence before the JSON or a missing key, and the loader crashes.

    What is the most reliable fix?

    1. ARepeat the JSON instruction at the end of the prompt in capitals.
    2. BStrip everything before the first brace with a regular expression.
    3. CDefine the claim as a tool’s input_schema with strict mode and force that tool.
    4. DAdd three examples of well-formed claim JSON to the prompt.
    Show answer and reasoning
    1. AIncorrect. Emphasis improves the odds but still leaves output to chance; the crash rate falls, it does not go to zero.
    2. BIncorrect. It patches one symptom and does nothing about missing keys or wrong types.
    3. CCorrect. The output becomes a schema-constrained tool input, so there is no preamble to strip and no missing required key.
    4. DIncorrect. Examples make the shape more consistent but do not enforce it.
  2. Question 2

    A mailroom agent receives invoices, receipts and purchase orders mixed together, unlabelled. There is a separate extraction tool for each type, and every document must produce structured data.

    Which tool_choice setting fits best?

    1. Aauto, so Claude can decide whether a tool is needed.
    2. Bany, so Claude must call one of the tools and chooses which.
    3. Ctool naming the invoice extractor, then re-run if it fails.
    4. Dnone, then parse the text response into the right schema.
    Show answer and reasoning
    1. AIncorrect. Claude could reply in prose, and the requirement is that every document yields structured data.
    2. BCorrect. It guarantees a tool call while letting the model pick the right schema — the choice doubles as classification.
    3. CIncorrect. Forcing one schema on every document mis-files receipts and purchase orders.
    4. DIncorrect. That abandons schema enforcement entirely.
  3. Question 3

    After moving to strict tool use, an extraction pipeline never produces malformed JSON. An audit finds that on contracts with no renewal date, renewal_date is filled with the signature date. The field is a required string.

    What change addresses the root cause?

    1. AMake renewal_date nullable and show in an example that null means “not stated”.
    2. BTurn off strict mode so the model can omit the field.
    3. CAdd a pattern constraint so only valid dates are accepted.
    4. DAsk the model to double-check dates before answering.
    Show answer and reasoning
    1. ACorrect. The schema was forcing a value to exist; allowing null gives the model an honest answer.
    2. BIncorrect. That brings back malformed output and still does not tell the model what absence should look like.
    3. CIncorrect. The signature date is a valid date; a format rule cannot tell which date is right.
    4. DIncorrect. The schema still requires a string, so a guess remains the only way to comply.
  4. Question 4

    A strict JSON output response comes back with stop_reason of max_tokens. What should the application assume?

    1. AThe JSON is valid, because strict mode guarantees it.
    2. BThe model refused and should not be retried.
    3. CThe payload may be incomplete; retry with a higher max_tokens.
    4. DThe schema is too complex and must be simplified.
    Show answer and reasoning
    1. AIncorrect. The guarantee covers completed responses; a truncated one may be incomplete and not match the schema.
    2. BIncorrect. A refusal has its own stop reason, refusal.
    3. CCorrect. The docs say output cut off at max_tokens may not match the schema and recommend retrying with a higher limit.
    4. DIncorrect. Over-complex schemas are rejected up front with an error, not truncated mid-response.

Sources

Drafted with AI assistance and checked against the sources above; expert review is in progress. Spotted an error? Tell us and it gets fixed, dated and listed on how this is written.