Asking a language model for JSON and getting back a markdown fence, a trailing comment or a missing key is the oldest integration bug in the field. Structured outputs are the vendors' answer: you supply a JSON Schema, and the API constrains the response to it. This guide reads the Claude, OpenAI and Gemini documentation side by side to show what each promises, where their schema rules differ, and which failures remain your job. It builds on how tool calling works, which covers the neighbouring feature that constrains the arguments a model passes to your functions.
What each vendor documents
Anthropic states that structured outputs "constrain Claude's responses to follow a specific schema, ensuring valid, parseable output for downstream processing." You pass the schema in output_config.format with type: "json_schema", and Claude returns "valid JSON that matches your schema in the response's text content block." The Python SDK's client.messages.parse() can take a Pydantic model and return a parsed_output.[1]
OpenAI describes the same idea with two entry points: function calling when you are connecting the model to your tools, and text.format with json_schema when you want a structured reply to the user. It also draws the line against the older mode: "only Structured Outputs ensure schema adherence", while JSON mode (json_object) produces valid JSON without enforcing a schema. OpenAI recommends Structured Outputs whenever possible.[2]
Google's page describes giving the model a JSON Schema so that it generates responses that follow it. You set response_format with a mime_type of application/json and a schema, and the SDKs accept Pydantic (Python) and Zod (JavaScript) definitions. Streamed output arrives as partial JSON strings that you concatenate.[3]
| Spec | Claude | OpenAI | Gemini |
|---|---|---|---|
| Where the schema goes | output_config.format[1] | text.format with json_schema[2] | response_format with a schema[3] |
| Pydantic or Zod helpers | Pydantic via messages.parse(); TypeScript helper jsonSchemaOutputFormat()[1] | Examples use schemas directly; helpers not read for this article[2] | Pydantic and Zod[3] |
| Separate switch for tool arguments | strict: true on tools[1] | Function calling with strict mode[2] | Structured output combined with built-in tools is a preview on Gemini 3 series models[3] |
| Documented caution on values | Refusals and truncation can break the schema[1] | Refusals and incomplete responses[2] | "always validate values in your application"[3] |
Where the schema subsets differ
None of the three supports all of JSON Schema. Each says so, and the differences are the part that bites when you port a schema between providers.
Claude supports basic types, enum, const, anyOf and allOf (with limits), internal $ref/$defs, default, required and string formats, and requires additionalProperties: false on objects. It documents no support for recursive schemas, external $ref, numeric constraints (minimum, maximum, multipleOf), string-length constraints, or array constraints beyond a minItems of 0 or 1. An unsupported feature returns a 400 error with details. The SDK helpers can move unsupported constraints into field descriptions and validate locally; the raw API does not.[1]
OpenAI supports string, number, boolean, integer, object, array, enum and anyOf. It allows pattern and several format values on strings, numeric bounds on numbers, and minItems/maxItems on arrays. Two rules shape how you write schemas: "all fields or function parameters must be specified as required" (so an optional field becomes a union with null), and additionalProperties must be set to false. The root must be an object and cannot use anyOf. Documented limits are up to 5000 object properties with up to 10 levels of nesting, and up to 1000 enum values across all enum properties. allOf, not, if/then/else and a few other keywords are unsupported, and fine-tuned models lack additional string, number and array constraints. Recursive schemas and $defs are supported.[2]
Gemini documents the types string, number, integer, boolean, object, array and null (via a type array such as ["string", "null"]). Supported keywords include properties, required, additionalProperties, string enum and format, numeric minimum and maximum, and array items, prefixItems, minItems and maxItems. The examples use anyOf and $ref for recursive structures. The page warns that "Not all JSON Schema features are supported" and that very large or deeply nested schemas may be rejected.[3]
| Schema feature | Claude | OpenAI | Gemini |
|---|---|---|---|
Numeric minimum/maximum | Not supported[1] | Supported (not on fine-tuned models)[2] | Supported[3] |
minItems/maxItems | minItems of 0 or 1 only[1] | Supported (not on fine-tuned models)[2] | Supported[3] |
| Recursive schemas | Not supported[1] | Supported[2] | Shown in examples with $ref[3] |
All fields must be required | additionalProperties: false required; optional parameters are counted against a limit[1] | Yes, with null unions for optional fields[2] | required is a supported keyword[3] |
The practical reading, and this is our interpretation rather than a vendor statement: a schema that relies on numeric ranges or length limits for correctness is not portable, so put those checks in your own validation layer and treat the schema as a shape contract.
Ordering, complexity and enum quirks
Some details are easy to miss. OpenAI says output follows the key order defined in the schema.[2] Anthropic says "required properties appear first, followed by optional properties", so mark everything required if order matters to you, for example when a field holds reasoning that should precede the answer field.[1]
Anthropic also documents complexity limits: 20 strict tools per request, 24 optional parameters across all strict schemas combined, and 16 parameters with union types. Beyond those it can return "Schema is too complex for compilation", and compilation has a 180-second timeout. Its suggested mitigations are to make parameters required, flatten nested structures, limit strict tools or split requests.[1] A quirk worth a unit test: Claude may change the capitalisation of enum and const values, so Anthropic advises comparing them case-insensitively and avoiding values that differ only by case.[1]
What can still go wrong
Refusals. Anthropic says "the output may not match your schema because the refusal message takes precedence over schema constraints"; the response has stop_reason: "refusal", returns HTTP 200 and is billed. It also warns that a property asking for the model's thinking can trigger a reasoning_extraction refusal and suggests asking for a short explanation instead.[1] OpenAI documents a refusal field in the response because a refusal "does not necessarily follow the schema".[2]
Truncation. With stop_reason: "max_tokens" Anthropic says the output "may be incomplete and not match your schema" and to retry with a higher limit. OpenAI likewise tells you to check for incomplete responses when max_output_tokens is reached.[1][2]
Right shape, wrong content. This is the one no schema fixes. Google states the output is "syntactically correct JSON" and tells you to validate values and handle outputs that are schema-compliant but semantically incorrect.[3] A model can return a well-formed invoice object with the wrong total. See why models hallucinate for why that happens and how to reduce it.
Latency and cost
Anthropic says the first use of a specific schema adds latency while the grammar compiles, and compiled grammars are cached for 24 hours from last use. Changing the schema structure or the tool set invalidates that cache; changing only a name or description does not. A special system prompt is injected, so input tokens rise slightly, and changing output_config.format invalidates the prompt cache for that conversation thread.[1] OpenAI documents the same first-request effect: the first request with any schema has additional latency, and later requests with the same schema do not.[2] Our reading: keep schemas stable across requests, and avoid generating a new schema per user. For how token counts become spend, see what an AI request really costs.
A defensive pattern
This is editorial guidance, not a vendor recommendation:
import json
def parse_reply(stop_reason: str, text: str, schema_check) -> dict:
# Refusals and truncation can break the schema, so look at the stop reason first.
if stop_reason in ("refusal", "max_tokens"):
raise ValueError(f"unusable response: {stop_reason}")
data = json.loads(text)
schema_check(data) # your own value checks: ranges, lengths, cross-field rules
return dataThe stop-reason names above follow Anthropic's documentation; OpenAI reports refusals through a refusal field and incompleteness through its own response status, so adapt the first branch per provider.[1][2]
Choosing an approach
- You need shape guarantees across several vendors: write a conservative schema (objects, strings, enums, arrays, all fields required,
additionalProperties: false) that sits inside all three subsets, then validate ranges yourself. - You need numeric or length constraints enforced by the API: the documentation shows OpenAI and Gemini accepting them and Claude not; confirm against your exact model before relying on it.
- The model calls your functions: use the strict-tool features described in the tool calling guide, and use response-format schemas for what the model says to the user or your pipeline.
What this article did not test
We read three documentation pages on 2026-10-13 and ran no requests, so we make no claim about how often any model violates a schema in practice, or about speed differences between vendors. The OpenAI and Anthropic pages are long; the Anthropic page's final few thousand characters, covering data retention details, were not read. Model support lists, limits and parameter names change often, so confirm against the linked pages before shipping.




