A Practical Checklist for Choosing Between Claude, OpenAI, and Gemini Structured Output Modes

A tangled mass of network cables in a server rack, representing unconstrained model output before a schema is applied

Choosing a structured-output mode comes down to three questions: does the provider validate your schema before generation or only after, which JSON Schema keywords actually survive, and what happens when the model can’t comply. Claude, OpenAI, and Gemini answer all three differently, and the differences only show up once a schema is complex enough to hit a provider’s specific limits. This checklist walks through each decision point with the exact constraint behind it.

1. Confirm which mechanism you’re actually using

“Structured output” means a different API shape on each platform, and mixing them up is the most common source of a schema that silently gets ignored:

ProviderParameterWhat it constrains
Claudeoutput_config.format (type json_schema)Claude’s final text response, returned as a JSON string inside a text content block
Claude (separately)strict: true on a tool definitionOnly that tool’s name and input parameters, not the model’s prose response
OpenAIresponse_format with type: "json_schema"The full response body
GeminiresponseMimeType: "application/json" + responseSchemaThe full response body

Claude’s two mechanisms are independent and combinable: a request can force strict validation on a tool call while separately constraining the model’s final summary to its own schema. If you only set strict: true on a tool and expected the model’s closing text to also come back as clean JSON, it won’t — that needs output_config.format too. This is a different decision than which schema fields to use for the tool definition itself, a topic our comparison of function-calling schemas covers from the tool-shape side rather than the output-format side.

2. Check additionalProperties before you test anything else

Every provider in this comparison requires additionalProperties: false on object schemas used in strict or structured modes, and Claude’s documentation is explicit that no other value is supported. Leaving this off — or setting it to true — is the single most common reason a schema that worked in a quick test fails once a provider’s SDK applies its own validation on top. If you’re hand-writing a schema rather than generating it from a Pydantic model or a Zod schema, add this field first and treat its absence as a bug, not an optional tightening.

3. Check which constraints actually survive, not just which you wrote

This is where the three providers diverge the most, and where a schema that validates locally can still behave differently in production:

ConstraintClaude (structured outputs)OpenAI (strict mode)Gemini (responseSchema)
Numeric bounds (minimum, maximum)Not supportedNot supported in strict modeNot in the documented supported-field list
String length (minLength, maxLength)Not supportedNot supported in strict modeNot in the documented supported-field list
pattern (regex)Simple patterns only, no backreferences or lookaroundSupported, validated structurallyNot in the documented supported-field list
Recursive schemasNot supportedSupported via $refNot documented either way
Optional fieldsAny property can be left out of requiredEvery property must be listed in required; make a field optional with a type-or-null union insteadAll fields required by default in some SDKs; use optionalProperties to opt out
Unsupported field behaviorReturns a 400 error naming the problemValidated against the schema at creation timeSilently ignored, the model handles the request but ignores the field

That last row is the one worth building a test around: Gemini’s documented behavior for an unsupported schema field is to silently drop it rather than reject the request, while Claude’s structured outputs return an explicit 400 for anything it can’t compile into its grammar. If you’re porting a schema from Claude to Gemini (or the reverse), a constraint that produced a loud, catchable error on one provider can produce a quiet, undetected gap on the other — write a test that checks the constraint actually held in the output, not just that the request succeeded.

4. Budget for the complexity limits before you hit them in production

Claude’s structured outputs apply hard caps across a single request, combined across every strict tool and JSON output schema used in it: 20 strict tools, 24 optional parameters total, and 16 parameters using anyOf or type-array unions. Past an internal grammar-size ceiling, the API returns a 400 with “Schema is too complex for compilation” rather than a degraded response — so a schema that’s growing organically as a product adds fields needs a periodic audit, not just a one-time design pass. There’s also a 180-second compilation timeout on first use of a new schema shape; Claude caches the compiled grammar for 24 hours after last use, so a schema that changes on every request — rather than reusing a small set of shapes — pays that compilation cost repeatedly instead of hitting the cache.

5. Decide what happens when the model can’t comply

Don’t assume “the request succeeded” means “the output matches the schema.” Claude can return stop_reason: "refusal" with a 200 status and billed tokens, and the content may not match the schema at all in that case — code that only checks the HTTP status will miss this. Separately, hitting the token limit (stop_reason: "max_tokens") can cut a valid-looking JSON object off mid-field, which is a truncation failure rather than a schema failure and needs a different retry strategy: raise max_tokens, don’t just resend the same request.

6. Watch for silent inconsistencies that aren’t technically violations

Enum casing is explicitly not guaranteed on Claude’s structured outputs — the same logical value can come back as “Conversation topic 3” in one response and “Conversation Topic 3” in another, both technically matching the enum. Code that does an exact string match against enum values downstream, rather than comparing case-insensitively, will intermittently fail on output that a schema validator would call perfectly valid. This is the kind of failure that won’t show up in a handful of manual tests and only surfaces at volume, which is exactly the class of problem worth load-testing for before it reaches customers rather than after, the same way our comparison of rate limits across the major AI APIs argues for stress-testing throughput assumptions before they become an incident.

7. Separate “doesn’t support this field” from “ignores this field”

If you’re using an SDK-level helper — Claude’s SDK transforming a Pydantic model, for instance — check whether unsupported constraints are being silently moved into the field’s description text rather than enforced. A description-only constraint is a hint to the model, not a guarantee; validators built into the same SDK may still enforce the original constraint against the response after the fact, which means a field can simultaneously be “unenforced at generation time” and “checked at validation time.” Know which of those two your code is actually relying on, because they fail differently: the first produces output your code then has to reject, the second rejects before your code ever sees bad output.

9. Match your retry logic to how each provider actually fails

A naive retry loop that just resends the identical request on any non-2xx response treats three very different failure modes as the same problem. A 400 for an over-complex Claude schema won’t ever succeed on retry without changing the schema itself — retrying unchanged just burns quota. A truncated response from hitting max_tokens needs a larger token budget, not a resend of the same parameters. A refusal with stop_reason: "refusal" is a 200 and will be billed again on retry, so blindly retrying those can double your cost without improving your odds, since the model’s underlying reason for refusing rarely changes between two identical calls. Write separate handling for each of these three outcomes rather than one generic retry-on-failure path, and log which one fired — that log is what tells you whether a production incident is a schema problem, a budget problem, or a content problem.

8. Check model support before you ship, not after

Native structured outputs on Claude are limited to specific model families and are not available through every deployment path — on Amazon Bedrock, for example, the feature is restricted to the legacy integration and a specific list of model versions, and it isn’t available at all through Claude in Amazon Bedrock as of this writing. Confirm support for your exact deployment path against Anthropic’s current documentation rather than assuming parity between the direct API and a cloud-marketplace integration of the same model. The same caution applies whenever a request also depends on beta features tied to specific model versions, the kind of version-to-version drift we cover in our walkthrough of Claude’s interleaved thinking beta header.

None of this replaces reading a provider’s own schema reference before shipping a complex one — Gemini’s structured-output documentation and Claude’s structured-outputs page both change as limits are raised or fields are added. Treat this checklist as what to verify, not a substitute for the page itself, and if your schema needs are simple — flat objects, a handful of string and boolean fields — you may not need to think about any of the edge cases above; they mostly bite once a schema has enums, unions, or more than a dozen optional fields, a complexity level our broader look at what function calling actually is treats as the exception rather than the starting point.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top