PrepGenAICerts

Structured Outputs: Constraining Output Shape at the API Level

Core

Work with the Messages API's core request/response mechanics · Difficulty 3/5

0%
structured-outputsconstrained-decodingstrict-tool-usejson-schema

Explanation

Moving Output-Shape Control from the Prompt into the API

Every prompting technique -- system prompts, few-shot examples, output constraints written in words -- shapes output by *asking* Claude to follow a format and hoping it does. That works on the inputs you tested and can still slip on an edge case you didn't, because a written instruction is a request, not an enforcement mechanism. Structured outputs close that gap differently: instead of asking nicely, the API itself enforces the shape through constrained decoding. At each token Claude generates, the API narrows the set of tokens it's even allowed to pick from to whatever stays valid against your schema, so an output that breaks the schema is never a possibility the model had access to in the first place. This is a fundamentally different guarantee than "the model was told to produce JSON and usually does."

Two distinct mechanisms fall under structured outputs, and they constrain two different things:

MechanismWhat it constrainsHow you enable it
JSON outputsThe final response textoutput_config.format with type: "json_schema" and your schema
Strict tool useThe arguments Claude sends to a toolstrict: true on the tool definition

JSON Outputs

Set output_config.format to constrain the response itself to a schema:

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["BILLING", "TECHNICAL", "ESCALATION"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
    messages=[{"role": "user", "content": f"Extract fields from: {ticket_text}"}],
)
# response text is guaranteed to be valid JSON matching the schema (barring
# a refusal or a max_tokens truncation -- see below).

Reach for this when the model itself is producing a structured payload your downstream code consumes -- extracting fields from a document, formatting an API response -- because it removes the need for defensive parsing and retry logic on every single call.

Strict Tool Use

Set strict: true directly on a tool definition, alongside name, description, and input_schema, to guarantee the arguments Claude passes to that tool validate against the schema before your code ever runs:

tools = [{
    "name": "book_flight",
    "description": "Book a flight to a destination on a given date for a number of passengers.",
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "destination": {"type": "string"},
            "date": {"type": "string", "format": "date"},
            "passengers": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6, 7, 8]},
        },
        "required": ["destination", "date", "passengers"],
        "additionalProperties": False,
    },
}]

This matters most in agentic loops, where a malformed tool argument doesn't just produce an odd response -- it can crash the function or trigger the wrong action outright. strict: true shifts that risk earlier -- by the time your code sees the call, the arguments have already been checked against the contract you wrote, so there's nothing left to validate before you act on them. This is the API-enforced counterpart to the manual schema-design discipline (required/optional fields, exclusion conditions in descriptions) covered under content boundaries and schema design -- that discipline still governs which fields exist and what they mean; strict: true is what guarantees the arguments Claude actually sends respect that shape every time, not just on the inputs you happened to test.

The Real Costs -- Weigh Them, Don't Enable by Default

Constrained generation is not free, and a production decision to turn it on should weigh four costs against the reliability gain:

  1. The first call on a new schema pays a latency tax. Before the API can enforce a schema, it has to turn that schema into a grammar it can check tokens against, and building that grammar happens once, on the first request. That compiled grammar then sits in cache for 24 hours since it was last touched -- so a schema that stays stable across a lot of traffic pays the build cost exactly once and coasts on the cache after that, while a workload that swaps schemas on nearly every call keeps re-paying the build cost and never gets to benefit from caching at all.
  2. Input tokens go up a bit. Turning structured outputs on means the API quietly adds its own system prompt describing the required shape, and that addition gets billed as ordinary input -- a small bump per call, but one worth folding into a cost projection once volume is high.
  3. A schema guarantee isn't a success guarantee. Two distinct `stop_reason values can still hand you output that doesn't match the schema, constraint or not: "refusal" (the model opted not to answer for safety reasons) and "max_tokens" (generation ran out of room and stopped partway through the structure). The constraint only governs *which tokens are legal to emit next* -- it says nothing about *whether the model finishes* or *whether it agrees to respond at all*. Your code still has to check stop_reason` rather than assuming a schema attached means a parseable response is guaranteed.
  4. It won't run alongside message prefilling. JSON outputs and prefilling the assistant's turn can't both apply to one request -- prefilling seeds the start of Claude's response, while a JSON-output schema constrains the entire response, and a single generation can't be governed by both mechanisms simultaneously. Choose whichever one the task actually calls for.

Common exam traps

  • Treating output_config.format as interchangeable with a prompt instruction that says "return only JSON" -- the API-level mechanism enforces the schema at generation time; the prompt instruction is a request the model can still miss on an untested edge case.
  • Assuming a schema-constrained response always parses successfully -- `stop_reason: "refusal" and stop_reason: "max_tokens"` both still occur and must be checked.
  • Ignoring the first-request compilation latency when reasoning about a workload that rotates through many distinct schemas -- the 24-hour grammar cache only helps a *stable* schema used repeatedly.
  • Trying to combine JSON outputs with an assistant-turn prefill on the same request -- the two are incompatible.

Key Takeaways

  • Structured outputs move output-shape control from the prompt into the API via constrained decoding -- the API restricts which tokens are legal against a schema, so an off-schema response cannot be produced
  • output_config.format with type json_schema constrains the final response text; strict: true on a tool definition constrains the arguments Claude sends to that tool
  • The first request against a new schema is slower due to grammar compilation; compiled grammars are cached for 24 hours from last use
  • Structured outputs raise input token count because the API injects a format-description system prompt that is billed as input
  • A guaranteed schema does not guarantee success -- stop_reason of refusal or max_tokens can still leave you without valid, matching output
  • JSON outputs (output_config.format) are incompatible with message prefilling on the same request

Glossary Terms

Related Concepts

PrepGenAICerts.com is an independent third-party exam-prep platform for the Claude Certified Architect (CCA-F) certification. We are not affiliated with, endorsed by, or acting on behalf of Anthropic PBC.

Note: New premium upgrades are temporarily paused while we resolve an issue with our payment provider. Existing premium members retain full access.