JSON Schema is not one thing — Anthropic, OpenAI, and Gemini enforce different subsets, and the moment you port a schema you meet the mismatch.
Anthropic ignores minLength, maxLength, minimum, maximum at validation time. Gemini enforces the OpenAPI 3.0 subset only. OpenAI's strict mode requires additionalProperties: false and marks all fields required. Each is documented, none of them is JSON Schema draft-2020-12, and none of them fails loudly when you send a keyword they do not enforce — the schema is accepted, the constraint quietly disappears, and the model happily produces output the schema was supposed to forbid. This essay is the table you need next to your editor when you port a schema between vendors — what each enforces, what silently drops, and the portable subset that survives all three.
The three subsets, at altitude.
Anthropic, OpenAI, and Gemini all take a JSON Schema in the same syntactic slot — input_schema, parameters, parameters respectively — and all three call it "JSON Schema." What that means differs enough that a schema written for one vendor is a candidate bug on the other two. The K7 vendor matrix covers the outer wrapper differences; this essay is one layer inside, on the schema-keyword enforcement.
Three shapes are worth pinning first. Anthropic uses "JSON Schema" as a rough description of a very permissive subset — the schema documents intent, and the model's constrained decoder honours some keywords and treats others as documentation. OpenAI has two modes: non-strict (schema is a hint the model tries to follow) and strict (schema is enforced by the constrained decoder). Gemini uses the OpenAPI 3.0 Schema object — a well-defined subset of JSON Schema draft-04, missing a lot of what draft-2020-12 added, with uppercase type names as one visible surface difference.
The schemas & contracts essay covers the design principle — make illegal states unrepresentable, make safety-relevant fields required — which is where the K10 discipline matters most: a keyword that silently drops on your target vendor is a load-bearing constraint the model is no longer honouring. The structured tool I/O essay covers the return-side of the same story. Here we zoom into the exact keyword table you need when porting.
Anthropic subset: strings and numbers open at the edges.
Anthropic's input_schema honours the structural keywords — type, properties, required, enum, items, description — and drops the range keywords: minLength and maxLength for strings, minimum, maximum, exclusiveMinimum, exclusiveMaximum for numbers. It also does not enforce pattern or format constraints at the constrained-decoder level. The model is told about them in the schema text and often obeys, but there is no hard mask against violations — the constraint is advisory. Similarly, const is not enforced; use a single-element enum if the constraint is load-bearing.
The practical consequence is that a schema which visibly caps a string at 200 characters on OpenAI strict mode will silently accept 5000 characters on Anthropic. If the cap matters — because your downstream stores it in a bounded column, or because you are paying per output token — the schema is not the enforcement point on Anthropic; you have to validate the model's output in application code, or express the constraint as an enum of allowed values. The same applies to number ranges: a quantity field with minimum: 1 and maximum: 100 is documentation on Anthropic, not enforcement, and a negative quantity is a bug you will catch at your database rather than at the model.
What Anthropic does enforce well: enum membership, required fields, top-level type, and object shape (property names, no extras when you set additionalProperties: false). Design your Anthropic schemas around those; treat everything else as prose in the description and validate at the harness edge.
OpenAI strict mode: additionalProperties everywhere, all required.
OpenAI's strict mode is the tightest of the three by design — the schema is compiled into a real grammar, and the model's output is guaranteed to match. The cost of that guarantee is two rules that catch teams by surprise. First, every object in the schema must set additionalProperties: false, including nested ones — omit it and the strict validator rejects the schema. Second, every property listed in properties must also appear in required; strict mode does not distinguish "optional field" the way JSON Schema does elsewhere. The workaround for a genuinely optional field is a union type: "type": ["string", "null"], and the caller decides null means "not present."
Two derived rules follow. Strict mode does not support oneOf, allOf, anyOf in most positions (as of the 2026 docs; check the compatibility matrix — it evolves). Deeply nested schemas hit an implementation depth cap around a handful of levels; the fix is usually to flatten. String keywords work — minLength, maxLength, pattern, enum, format — and number keywords work. strict: true flag is what turns enforcement on; without it, the schema is a hint and the model may produce output that violates it.
The failure mode you will see is subtle: your schema was drafted for Anthropic (permissive, no additionalProperties discipline, some optionals), you turn on OpenAI strict mode, and the SDK refuses to register the tool. That is the good failure — it stops before running. The bad one is you drafted for OpenAI strict, sent it to Anthropic, and now the string-length cap silently disappears because Anthropic ignores those keywords. Both directions bite; the K7 essay's intermediate-representation pattern is the mechanical fix.
Gemini's OpenAPI 3.0 subset: uppercase types and no draft-2020-12 tricks.
Gemini's schema is drawn from OpenAPI 3.0's Schema object, which is essentially JSON Schema draft-04 with OpenAPI's specific keywords. The type names are UPPERCASE strings (OBJECT, STRING, INTEGER, NUMBER, BOOLEAN, ARRAY) rather than lowercase, which is the first thing a copy-paste port trips on. Beyond that, constructs that don't exist in OpenAPI 3.0 are rejected: const, $defs, $ref across files, and the more exotic draft-2020-12 additions all fail validation.
What Gemini enforces reliably: type, properties, required, enum, items, description, string minLength / maxLength, number minimum / maximum, format for well-known formats. What it does not: const, oneOf with heterogeneous types, patternProperties. nullable: true is an OpenAPI-style knob rather than the JSON Schema "type": ["string", "null"] union; getting this wrong is a common port bug.
| keyword | Anthropic | OpenAI strict | Gemini (OpenAPI) | |------------------------|------------|---------------|------------------| | type / properties | enforced | enforced | enforced (UPPER) | | required | enforced | enforced (all)| enforced | | enum | enforced | enforced | enforced | | additionalProperties | honoured | REQUIRED false| honoured | | minLength / maxLength | ignored | enforced | enforced | | minimum / maximum | ignored | enforced | enforced | | pattern | advisory | enforced | partial | | format | advisory | enforced | well-known only | | const | ignored | enforced | rejected | | oneOf / anyOf / allOf | partial | limited | not supported | | $defs / $ref | supported | limited | not supported | | type: ["x", "null"] | supported | required opt | use nullable |
Read the row for a keyword and you know its portability. Anything with three different behaviours across the columns is a keyword you either avoid in your portable spec or gate behind a per-vendor emitter. Anything that says "ignored" is silent-drop territory: it will not error, and it will not enforce, and your test suite has to catch the constraint at the application boundary rather than at the schema.
The portable subset: what actually ports intact.
Draw the intersection of the three enforcement columns and the result is the portable subset. Top-level "type": "object". A properties map of primitives — string, integer, number, boolean — plus array of primitives with items. A required array. enum on strings. description everywhere. That is it. Ports across all three vendors intact, enforces on all three, and covers the majority of real tool schemas without ceremony.
# The portable subset — enforces the same way on all three vendors. { "type": "object", "properties": { "order_id": { "type": "string", "description": "The order identifier, e.g. 'o_42'." }, "reason": { "type": "string", "enum": ["defective", "wrong_item", "other"], "description": "The reason for the refund." }, "amount_cents": { "type": "integer", "description": "Refund amount in cents (must be positive)." } }, "required": ["order_id", "reason", "amount_cents"] }
What is missing from that schema is deliberate. There is no minimum: 1 on amount_cents because Anthropic will not enforce it and a portable schema cannot silently lose a constraint on one vendor. The description says "must be positive" — that is advisory prose the model will usually honour, and application-level validation catches the case where it doesn't. There is no additionalProperties: false at the top because that would break Anthropic's tolerant validator on some patterns and force OpenAI strict mode to be on for the schema to be legal. Add both back at the per-vendor emitter step, not in the shared spec.
Read the five steps together and JSON Schema stops being one thing. Three vendors, three subsets, one narrow intersection you can rely on, and per-vendor extensions gated behind emitters. The teams that keep their schema portability healthy do two things — they never author schemas directly for a single vendor, and they run a per-vendor CI validator pass every commit — and both practices cost hours to set up and save weeks of "why did the enum stop working" debugging over the life of a codebase.