Curriculum › Prompt Engineering & Structured Output · 20% of the exam

Structured output and schema enforcement

What you’ll be able to do

  • Define an extraction tool whose input schema is the output you want, and read the result from the tool_use block
  • Choose between tool_choice “auto”, “any”, and forcing a named tool for a given pipeline step
  • Design schemas with nullable fields, “unclear” values, and “other” + detail enums so the model isn’t pushed to fabricate
  • Explain why schema enforcement removes syntax errors but not semantic errors, and pair schemas with normalization rules

What you need to know

Tool use is the structured-output mechanism

Asking Claude to "reply only with JSON" is a request. Responses can arrive wrapped in a code fence, preceded by a sentence of commentary, or with a trailing comma, and your parser breaks on some fraction of them. Rewording the request more forcefully reduces the rate but doesn't remove it.

The reliable approach is tool use with a JSON schema. Define a tool whose input schema is the structure you want, have the model call it, and read the structured data from the tool_use block's input. The tool doesn't need to do anything; it exists to receive the payload. A strict schema through tool use eliminates JSON syntax errors, because the output is generated to match the schema rather than written as free text and parsed afterwards.

an extraction tool, and reading the result
tools = [{ "name": "extract_invoice", "description": "Record the fields extracted from one invoice.", "input_schema": { "type": "object", "properties": { "invoice_number": {"type": "string"}, "due_date": {"type": ["string", "null"], "description": "YYYY-MM-DD, or null if the invoice states none"}, "category": {"type": "string", "enum": ["hardware", "software", "services", "other", "unclear"]}, "category_detail": {"type": ["string", "null"], "description": "Required when category is other"}, "total": {"type": "number"} }, "required": ["invoice_number", "due_date", "category", "total"] } }]

response = client.messages.create(…, tools=tools, tool_choice={“type”: “tool”, “name”: “extract_invoice”}) block = next(b for b in response.content if b.type == “tool_use”) invoice = block.input # a dict that matches the schema

tool_choice: three settings, three guarantees

  • "auto": the model decides whether to call a tool at all. It may return plain text instead. That's right for a conversational agent, and wrong for a pipeline step that must always yield structured data.
  • "any": the model must call a tool but chooses which. Use it when several extraction schemas exist (invoice, receipt, contract) and the document type isn't known in advance. You're guaranteed structured output, and the model picks the schema that fits.
  • Forced selection, {"type": "tool", "name": "extract_metadata"}: the model must call that specific tool. Use it when a particular extraction has to run before later steps. For example, force extract_metadata first so enrichment steps always have its output to build on.

The common mistake is leaving "auto" in place for an extraction step and then handling an occasional text reply as an error. If the step must produce structure, use "any" or force the tool.

Design the schema so the honest answer is expressible

A schema doesn't just shape output; it defines which answers the model is allowed to give. If a field is required and non-nullable, the model must put something there, even when the document doesn't contain it. That is how fabricated due dates and invented reference numbers get into extraction output.

  • Required vs optional. Make a field nullable (or optional) when source documents may not contain the information. A null due date is accurate. A plausible-looking invented date is a silent error.
  • "unclear" for ambiguity. When the source is genuinely ambiguous, an "unclear" enum value lets the model say so instead of guessing between two categories.
  • "other" + detail for extensible categories. A closed enum forces new kinds of input into the nearest wrong bucket. Adding "other" plus a category_detail string captures the real value and shows you which categories to add next.

Valid shape is not correct content

Schema enforcement guarantees syntax and structure. It does not guarantee that the values are right. A schema-valid invoice can have line items that don't add up to the stated total, a vendor name in the customer field, or a date in the wrong field. These are semantic errors, and no schema catches them. They need validation logic after extraction (covered in 4.4).

Normalization rules go in the prompt

Source documents format the same value many ways: "03/04/26," "4 March 2026," "Mar 4." A schema can say the field is a string; it can't say which convention to convert to. Put format normalization rules in the prompt alongside the strict schema, for example "dates as YYYY-MM-DD; amounts as numbers without currency symbols; if the day/month order is ambiguous, set the field to null." The schema enforces the shape, and the prompt sets the convention for filling it.

Key concept

Get structured output by defining a tool whose JSON schema is the output, and choose tool_choice for the guarantee you need: “auto” (may return text), “any” (must call some tool), or a forced named tool. Make absent data nullable, give ambiguity and novelty somewhere to go (“unclear”, “other” + detail), and remember that a valid schema still allows wrong values.

When a scenario reports parse failures from prompted JSON, the fix is tool use, not a sterner prompt. When it reports plausible but invented values, check whether the schema forced the model to fill a field the document didn't support.

Practice scenario

ScenarioA pipeline receives scanned documents that may be invoices, purchase orders, or delivery notes, and has one extraction tool per type. It uses tool_choice: "auto". About 3% of responses are prose summaries with no tool call. Separately, purchase orders that lack a delivery date come back with dates that appear nowhere in the document, because delivery_date is a required string.
Work it through, then open this

Two separate fixes. First, switch to tool_choice: “any”. The document type is unknown and several schemas exist, so the model should be required to call one of the extraction tools while still choosing which. That removes the prose replies. (Forcing one named tool would be wrong here, because it would push delivery notes through the invoice schema.) Second, make delivery_date nullable. The model invented dates because the schema gave it no honest way to say “not present.”

Build exercise — Convert a prompted-JSON extractor to tool use

Intermediate · 30 min

What you’ll learn

  • Defining an extraction tool and reading its tool_use input
  • Choosing tool_choice for single-schema and multi-schema steps
  • Designing nullable, “unclear”, and “other” + detail fields
  1. Take a prompt that asks for JSON in text and define an equivalent tool whose input schema has the same fields, then read results from the tool_use block.

    • Why: This moves from a request the model can drift from to output generated against a schema.
    • You should see: No more code fences, commentary, or parse failures in the output.
  2. Set tool_choice deliberately: force the named tool if there’s one schema, or use “any” if you add a second schema for a different document type.

    • Why: “auto” permits a text reply; a pipeline step that must yield structure shouldn’t allow that.
    • You should see: Every response containing a tool_use block, with the right schema chosen when there are two.
  3. Run documents that are missing some fields or have novel categories. Make those fields nullable, add “unclear” and “other” + detail to the category enum, and add normalization rules to the prompt.

    • Why: Required non-nullable fields push the model to fabricate; closed enums push it to miscategorize.
    • You should see: null where the document is silent, “other” with a detail string for new categories, and consistently formatted dates. Then check one line-item total by hand: schema validity didn’t guarantee it was right.

Exam traps

Prompting for JSON in text and parsing whatever comes back

That’s a request, not enforcement. Tool use with a JSON schema is the reliable way to get schema-compliant output with no syntax errors.

Leaving tool_choice on “auto” when the step must always produce structured output

“auto” lets the model answer in text. Use “any” when several schemas exist, or force a named tool when one specific extraction must run.

Marking fields required when the source document may not contain them

A required non-nullable field forces the model to put something there. Make it nullable so “not present” is an allowed answer.

Using a closed enum with no “other” or “unclear” option for open-ended categories

New or ambiguous inputs get forced into the nearest wrong value. “other” + a detail string and an “unclear” value keep the output honest.

Assuming schema-valid output is semantically correct

Schemas eliminate syntax errors, not semantic ones such as totals that don’t sum or values in the wrong field. Those need validation logic.

Relying on the schema alone to normalize inconsistent source formats

A schema fixes the type, not the convention. Put normalization rules in the prompt alongside the schema.

Sources

Quick check

A review bot's security findings are almost always accepted, but its "performance" findings are dismissed about 85% of the time. Developers have started resolving every bot comment without reading it, including valid security findings. What is the best first step?