Dani Reyes10 min read5 views

Your JSON schema is a second cached artifact

Structured outputs and strict tool use turn your JSON schema into a separate cached artifact. It has its own 24 hour lifetime, invalidation rules that invert what you would guess, and a retention boundary that zero data retention does not cover.

Updated on September 22, 2026

Flat schematic on deep navy. A request block splits into two lanes. The lime lane compiles a JSON schema into a grammar block and stores it as a persistent stack. The grey lane carries message content to a block that dissolves into shrinking dashes, standing for data not stored.
Flat schematic on deep navy. A request block splits into two lanes. The lime lane compiles a JSON schema into a grammar block and stores it as a persistent stack. The grey lane carries message content to a block that dissolves into shrinking dashes, standing for data not stored.
On this page

Quick answer

As of September 2026, if you use structured outputs or strict tool use on the Claude API, your JSON schema stops being part of your request and becomes a separate cached artifact with its own lifetime and its own retention rules. Anthropic compiles it into a grammar, caches it for up to 24 hours since last use, and that cache is explicitly not covered by the same protections as your prompts. Zero data retention on structured outputs is listed as qualified rather than plain yes, and the schema cache is the qualification.

I spent a morning on this because I assumed the schema was just request data. It is not.

The moment

I was adding a strict tool to an extraction service. Boring work: a schema, strict: true, ship it. Then a colleague asked whether we could run the thing against a customer dataset that sits under a BAA, and I said yes without thinking, because prompts and responses are covered and we were not storing anything ourselves.

Then I actually read the retention page instead of remembering it.

The schema is not request data. It is compiled, and the compiled artifact is cached on a different path from the message content. That single sentence changed the answer, because our enum values were a list of condition codes. Not patient names, nothing dramatic. But they were in the schema, and the schema is the one part of that request that outlives it.

Here is what I found, in the order it mattered.

Anthropic

Finding 1: two features share one pipeline, and therefore one cache

Structured outputs and strict tool use look like separate features. They are one mechanism with two entry points.

JSON outputs constrain the response. Strict tool use constrains tool inputs.

json
{
  "model": "claude-opus-5",
  "max_tokens": 1024,
  "output_config": {
    "format": {
      "type": "json_schema",
      "schema": {
        "type": "object",
        "properties": { "name": { "type": "string" } },
        "required": ["name"],
        "additionalProperties": false
      }
    }
  }
}
json
{
  "tools": [
    {
      "name": "get_weather",
      "description": "Get the current weather in a given location",
      "strict": true,
      "input_schema": {
        "type": "object",
        "properties": { "location": { "type": "string" } },
        "required": ["location"],
        "additionalProperties": false
      }
    }
  ]
}

Note where strict sits. The strict tool use docs put it plainly: set it "as a top-level property in your tool definition, alongside name, description, and input_schema". It is not a field on tool_choice, which is where I first went looking.

Both paths compile your schema into a grammar and constrain sampling token by token. The strict tool use page says it uses "the same pipeline as structured outputs". That shared pipeline is why everything below applies to both, and why a team that has only ever used strict: true on tools still has a schema cache they have never thought about.

Finding 2: the cache invalidates on things you would not guess

Two competitors I read cover the 24 hour figure. Neither covers what resets it, and the rules are not the obvious ones.

Per the structured outputs documentation, the cache is invalidated if you change the JSON schema structure or the set of tools in your request when you are using structured outputs and tool use together. And then the part I had backwards:

Anthropic, verbatim: "Changing only name or description fields does not invalidate the cache"

So iterating on tool descriptions, which is the single most common thing you do while tuning an agent, is free. You can rewrite every description in your toolset all afternoon and keep the warm grammar.

Meanwhile adding one unrelated tool to the array, which feels like the cheap change, invalidates the compiled grammar for the whole request. That is the inversion worth internalising. The expensive edit looks trivial and the trivial edit looks expensive.

Cache lifetime is "24 hours from last use", so a schema in steady traffic effectively never goes cold, and one used hourly stays warm forever. Cold means a recompile and extra latency on that first request.

A note on a discrepancy I hit rather than resolved: an AWS write-up on structured outputs on Amazon Bedrock describes grammars as cached per account for 24 hours from first use, where Anthropic's own pages say since last use. I am not going to adjudicate that. Anthropic's retention page states that on Bedrock the cloud provider is the data processor, so these are plausibly two different systems described accurately by their own owners. Read whichever page governs the platform you are actually on.

Finding 3: zero data retention has an asterisk, and the asterisk is your schema

This is the finding I would want someone to hand me before a compliance review.

On the API and data retention page, structured outputs is listed as ZDR eligible with a qualifier rather than a flat yes, and the table explains why in one line:

Anthropic, verbatim: "Your prompts and Claude's outputs are not stored. Only the JSON schema is cached, for up to 24 hours since last use."

So zero data retention means zero retention of the things you were thinking about, and up to 24 hours of retention for the thing you were not.

The HIPAA guidance is more specific, and more pointed:

Anthropic, verbatim: "When using structured outputs or tools with strict: true, the API compiles JSON schemas into grammars that are cached separately from message content. These cached schemas do not receive the same PHI protections as prompts and responses. Do not include PHI in JSON schema definitions."

The restriction is enumerated, and the enumeration is the useful part, because it names exactly the four places a schema author would naturally put real values:

  • schema property names
  • enum values
  • const values
  • pattern regular expressions

Every one of those is somewhere a careful engineer puts domain data on purpose. An enum is the correct way to constrain a field to a known set. If that known set is a list of real identifiers, you have moved protected data out of the part of the request that is protected and into the part that is cached.

The fix is not clever. Keep schemas structural. Constrain the shape, let the message content carry the values, and if you need a closed set of sensitive codes, validate it your side after the response rather than encoding the set into the grammar.

Python

Finding 4: the model does not receive the schema you wrote

I had assumed that an unsupported JSON Schema keyword would be rejected. It is not. It is removed.

The supported subset is real but narrow. Confirmed on the structured outputs page for September 2026: no recursive schemas, no external references, no numerical constraints such as minimum or maximum or multipleOf, no string length constraints, and minItems only at values 0 and 1. additionalProperties must be false on objects.

What happens to the rest is the part that matters:

Anthropic, verbatim: "The SDKs handle unsupported constraints by stripping them from the schema sent to the API while validating responses locally. This means Claude receives a simplified schema, but your code still enforces all constraints through validation."

Read that carefully, because it quietly redefines the guarantee. "Guaranteed schema compliance" is guaranteed against the simplified schema, not the one you wrote. If your schema says a quantity must be at least 1, the grammar never learns that. The model is free to emit 0. Your local validator catches it, which is genuinely useful, but it catches it as a failure after generation, not as an impossibility during it.

That changes what you should expect operationally. Type errors disappear, because types are in the grammar. Range and length violations do not disappear, they change shape: instead of a parse error you get a validation error on well formed JSON, and you still need the retry path you thought you had deleted.

My rule after this: put in the schema what the grammar can enforce, and keep a validator for everything else. Do not delete the validator because the output is now "guaranteed".

Finding 5: where strict is refused, and the parameter that quietly moved

Two smaller things that will each cost you a confusing afternoon.

The computer use and browser use toolsets do not accept strict mode at all. The strict tool use page names the entries computer_toolset_20260801 and browser_toolset_20260801 and says a request setting strict: true on either "is rejected". If you are hardening an agent by turning strict on across a toolset in a loop, that loop will hit a wall on exactly the two toolsets whose inputs you probably most wanted constrained.

The second is a migration that is easy to miss because nothing breaks on the wire. The parameter moved from output_format to output_config.format, and beta headers are no longer required. The old form still works at the API level for a transition period. But the client does not agree with the server:

Anthropic, verbatim: "the Python SDK (v1.0 and later) does not accept output_format={...} on client.beta.messages.create() or count_tokens() and raises a TypeError; use output_config instead."

So a raw HTTP call written in 2025 keeps working, and the same request expressed through a current Python SDK raises before it ever leaves your process. If you maintain both a curl based smoke test and an SDK client, they will disagree, and the SDK is the one telling you the truth about where the API is going.

This is the same output_config object that carries effort and task_budget, which I wrote about in the task budget field log. It has quietly become the place where output shaping lives.

One more thing I noticed while checking model support. The live support list includes model identifiers newer than the ones my bundled reference table knows about, including claude-opus-5-5, which is what the current documentation examples use. I am recording that as an observation on today's date rather than a conclusion. The lesson I keep relearning on this beat is that a cached table is a snapshot and the docs page is the source of truth.

What I did not verify

Being explicit, because several of these would change how you act.

I did not measure compilation latency myself. One competitor quotes 100 to 300 milliseconds for a first request against a new schema. Anthropic's own page says only that there is "additional latency while the grammar compiles" without a number. I am not repeating the figure as fact.

I did not test the invalidation rules empirically. They are quoted from documentation, not measured. Testing them properly means timing cold and warm requests across a schema edit, a description edit and a tool-set edit, and I have not done that.

I did not verify what happens at the boundary of the cache window, or whether a cache is scoped per key, per workspace or per organisation. Anthropic's wording says the schemas are cached and does not say at what scope on the first party API.

I did not check whether the PHI restriction has any enforcement behind it. Nothing in the documentation suggests the API inspects your schema for sensitive values, and I would assume it does not. Treat it as a contract you are responsible for keeping, not a guard rail that will stop you.

And I have not run any of this under a BAA in production. This is a reading of the published documentation as it stood in September 2026, not legal advice, and the compliance question that started my morning still went to someone qualified to answer it.

If you want the other half of this, the typed Python path with Pydantic and the practical schema ergonomics, my colleagues at AgentNotebook have a structured outputs tutorial that covers the code shape properly. This post is deliberately the operations half.

Postscript: I went looking for a caching bug and found a retention boundary instead, which is roughly how every useful morning on this beat has gone.

D

Written by

Dani Reyes

Frequently asked questions

How long does Claude cache a JSON schema for structured outputs?

Anthropic's documentation states compiled grammars are cached for up to 24 hours since last use. Because the window is measured from last use rather than first use, a schema in steady traffic stays warm indefinitely, and only a genuinely idle schema goes cold and has to be recompiled.

What invalidates the structured outputs schema cache?

Changing the JSON schema structure invalidates it, and so does changing the set of tools in the request when you use structured outputs together with tool use. Changing only a name or description field does not invalidate it, so iterating on tool descriptions is free while adding an unrelated tool is not.

Is structured outputs covered by zero data retention?

It is listed as ZDR eligible with a qualification rather than a flat yes. Prompts and responses are not stored, but the JSON schema itself is cached for up to 24 hours since last use, and that schema cache is the reason the status is qualified.

Can I put patient data in a JSON schema under a BAA?

No. Anthropic's data retention documentation states that compiled schemas are cached separately from message content and do not receive the same PHI protections as prompts and responses, and it specifically names property names, enum values, const values and pattern regular expressions. Sensitive values belong in message content, not in the schema.

Where does strict go in a Claude tool definition?

strict is a top-level property of the tool definition itself, alongside name, description and input_schema. It is not a field on tool_choice. The schema also needs additionalProperties set to false and a required array.

Context engineering is four mechanisms, not one

Context engineering on the Claude platform is not a setting. It is four mechanisms at four points in the pipeline, and the one everybody enables first pays for context window with prompt cache.

14 min read13