Skip to content
Skip to main content
A precision steel template plate letting only matching geometric blocks pass through, a metaphor for Claude structured outputs constraining response shape
9 min readBy Carlos Aragon

Claude Structured Outputs vs Strict Tool Use: Which One You Actually Need

Use JSON outputs (output_config.format) when your code consumes Claude's answer. Use strict tool use (strict: true) when Claude calls your code. One constrains what comes out, the other constrains what goes in — and they work together in the same request. Both guarantee shape, not sanity, which is where most teams get burned.

Two Features, One Name, Different Jobs

“Structured outputs” on the Claude platform is an umbrella over two mechanisms, and the docs list them side by side in a way that makes them look like alternatives. They aren't. The question isn't which one is better — it's which direction the JSON is travelling.

JSON outputs constrain the message Claude sends back to you. You set a schema and the final text lands in response.content[0].text as valid JSON, every time. That's the one you want for extraction, classification, report generation — anything where the model's answer is the payload.

Strict tool use constrains the arguments Claude passes into a function you exposed. You add strict: true to a tool definition and the input object on that tool call matches your input_schemaexactly. That's the one you want when a malformed argument means a 500 in your handler instead of an ugly line on a dashboard.

The one-line rule:

If a bad shape breaks your display, use JSON outputs. If a bad shape breaks your code, use strict tool use. If both, use both — a single request can carry strict tools and an output_config.format at the same time.

Under the hood they're the same trick. Your JSON Schema is compiled into a grammar, and that grammar restricts which tokens the model is allowed to emit at each step. It isn't a prompt asking nicely for JSON and it isn't a validator rejecting bad output after the fact — invalid tokens are never generated. That's why the retry loop disappears rather than getting shorter.

What the Config Actually Looks Like

First, a migration note that will save you a debugging hour: the parameter moved from output_format to output_config.format. Old tutorials — and a lot of blog posts still ranking — show the old top-level key. It still works during the transition period, along with the old structured-outputs-2025-11-13 beta header, but both are deprecated. The header is no longer required at all since the feature went GA on February 4, 2026.

# JSON outputs — constrain the ANSWER
{
  "model": "claude-sonnet-5",
  "output_config": {
    "format": {
      "type": "json_schema",
      "schema": {
        "type": "object",
        "properties": {
          "sentiment": { "enum": ["positive", "neutral", "negative"] },
          "score":     { "type": "integer" }
        },
        "required": ["sentiment", "score"],
        "additionalProperties": false
      }
    }
  }
}

# Strict tool use — constrain the TOOL CALL
{
  "tools": [{
    "name": "create_invoice",
    "strict": true,
    "input_schema": { "type": "object", "...": "..." }
  }]
}

You rarely hand-write either one. The SDKs derive the schema from types you already have: client.messages.parse() takes a Pydantic model in Python and a Zod schema via zodOutputFormat()in TypeScript. That's the nice path — right up until the auto-generated schema contains a keyword the grammar compiler refuses, which is the next section.

The Guarantee Is Shape, Not Sanity

This is the part that cost me real time, so I'll be blunt about it. A schema that says a lead score is an integer between 0 and 100 will cheerfully return 4000. Not because the model is dumb — because minimum and maximum are not supported keywords. They're silently dropped when the grammar is compiled. The type is enforced. The range is decoration.

I found this in a scoring pipeline where I'd deleted the Zod refinement after switching to structured outputs, on the reasonable-sounding theory that the API now guaranteed the schema. It guaranteed the schema I was allowed to send, which was not the schema I wrote. The rows were well-formed. They were also wrong, and nothing threw.

Here's the full unsupported list, and it's worth memorizing:

  • Numeric constraints — minimum, maximum, multipleOf are all ignored.
  • String constraints — minLength and maxLength are ignored.
  • Array constraints — minItems only accepts 0 or 1. "At least three items" is not expressible.
  • Recursive schemas — a comment tree or nested category structure is rejected outright.
  • External $ref — anything pointing at an http:// URL fails; keep definitions inline.
  • Complex enums — enum values must be strings, numbers, booleans or null, never objects or arrays.
  • additionalProperties must be false on every object. Anything else is rejected.

Why your Pydantic model gets rejected

A Field(ge=0, le=100) in Pydantic or a .min(1).max(280) in Zod compiles straight into those unsupported keywords. The model validates perfectly in your test suite and the API rejects it. Send a stripped schema, keep the full one for validation after parsing.

So the correct mental model is two layers: structured outputs kill parse errors and retry loops; your validator still owns business rules. Anyone telling you this feature lets you delete your validation layer has not shipped it.

The Caching Trap That Costs More Than the Feature Saves

Two separate caches are in play, and mixing them up is expensive.

The first is the grammar cache. Your compiled schema is cached for 24 hours from its last use, so the first request with a new schema pays extra latency while it compiles. Changing the schema structure invalidates it — but changing a field's name or descriptiondoes not, which is a small mercy when you're tuning prompts.

The second is the prompt cache, and this is the one that bites. Changing output_config.format invalidates the prompt cache for that conversation thread. If you're generating a bespoke schema per request — per tenant, per document type, per user-defined field set — you are nuking your cache on every call. On a long agent thread with a large system prompt, that swamps any saving from dropping retries, and it quietly raises your input-token rate too, which matters because cached input tokens don't count toward your ITPM ceiling. Losing the cache costs you money and headroom at the same time.

The fix is boring and effective: define a small fixed set of schemas and route to one of them, rather than composing a new one on the fly. Five canonical shapes covering 95% of your cases beats one perfectly-tailored shape per request. If you need per-tenant fields, put them in a generic attributes object with a stable schema instead of regenerating the top-level structure. The same discipline that makes prompt caching pay off applies here — stability is the asset.

There's also a small, unavoidable token cost: Claude receives an injected system prompt explaining the output format, so your input count ticks up slightly. It's noise compared to the cache issue.

When to Use Which — The Decision List

Skip the matrix. Here's how the choice actually falls out in practice:

  • Extracting fields from an email, invoice or screenshot → JSON outputs. Your code reads the answer.
  • Classifying or scoring records for a database write → JSON outputs, plus a range check you keep yourself.
  • An agent that books, charges, writes or deletes → strict tool use. Bad arguments must never reach the handler.
  • An agent that both calls tools and returns a final structured report → both, in the same request.
  • A recursive structure like a comment tree or nested taxonomy → neither will accept it. Flatten it with parent IDs.
  • A one-off script where a retry is free → honestly, neither. The setup isn't worth it below production volume.

The agent case deserves emphasis. If you've been writing defensive parsing at the top of every tool handler — coercing strings to numbers, filling in missing fields, catching the one model that occasionally sends "true" instead of truestrict: true deletes all of it. That defensive code was never business logic, it was a tax. My guide to Claude tool use in production covers the rest of that surface, and the Claude API agent guide puts it in the context of a full loop.

Model and Platform Support (Check Before You Assume)

On the Claude API, support is broad: Fable 5, Mythos 5, Opus 5, Opus 4.8, 4.7, 4.6 and 4.5, Sonnet 5, Sonnet 4.6 and 4.5, and Haiku 4.5. That Haiku line matters more than it looks — extraction and classification are exactly the workloads you want on a cheap model, and structured outputs are what make a small model safe to trust with a schema.

Platform parity is the gotcha. Bedrock's supported list is narrower than the direct API's, and Microsoft Foundry only supports it on “Hosted on Anthropic” deployments, not Azure-hosted ones. If your enterprise client routes through Bedrock, verify the specific model before you design around the feature. I've watched that assumption turn into a re-architecture two weeks before a launch.

The official reference is worth bookmarking rather than trusting a blog post six months from now — the Claude structured outputs documentation keeps the supported-keyword list current, and Anthropic's launch write-up explains the design intent.

The Short Version

  • JSON outputs (output_config.format) constrain the answer. Strict tool use (strict: true) constrains the tool call. Not alternatives — they compose.
  • The parameter moved from output_format to output_config.format. The beta header is deprecated and no longer required.
  • Both work by constrained decoding, so invalid JSON is never generated. Retry loops for parse errors disappear entirely.
  • The guarantee is shape, not values. minimum, maximum, minLength, maxLength and multipleOf are dropped. Keep your validator.
  • additionalProperties must be false, recursion is unsupported, and minItems only accepts 0 or 1.
  • Changing the schema invalidates the prompt cache for the thread. Use a few fixed schemas, not one per request.

Need an AI Pipeline That Returns Data You Can Actually Trust?

I build production AI systems on the Claude API, n8n and Supabase — structured extraction, agent tool layers, and the validation and caching discipline that keeps them cheap and predictable. If your pipeline is held together by retry loops and defensive parsing, let's fix it.

Related Posts

AI Models

Claude's Compaction API: Long Sessions Without Context Rot

Compaction summarizes a long conversation server-side once it crosses your trigger threshold, and returns the summary as a compaction content block you must pass back every turn. Append only the response text instead of the full response.content and the feature silently does nothing while you pay for it twice. Plus the billing trap: top-level usage excludes the compaction pass, under-reporting a compacting turn by roughly 8x — and why context editing is often the cheaper tool.

AI Models

Claude API 429 Rate Limits: How to Fix Them in Production

A Claude API 429 isn't one limit, it's three — RPM, ITPM and OTPM — enforced per model at the organization level on a token bucket that refills continuously. Honor retry-after before you reach for backoff, add jitter so your workers stop stampeding, log the anthropic-ratelimit headers so you throttle before the error fires, and share one limiter across every worker. Plus the fix almost nobody mentions: cached input tokens don't count toward ITPM, so prompt caching raises your effective ceiling roughly 5x at an 80% hit rate.

AI Models

Claude Sonnet 5 vs Opus 4.8 (2026): The Cost Math I Actually Use to Pick

Sonnet 5 is cheaper per token; Opus 4.8 usually finishes hard, open-ended work in fewer tokens and fewer retries — so the number that decides it is cost-per-completed-task, not cost-per-million. The at-a-glance table (pricing + agentic-coding benchmarks), the loop signal I use to escalate, and real numbers from the Claude Code agents I run in production: Sonnet 5 by default, Opus 4.8 for the hard tail.