OpenAI Strict JSON Schema: What Structured Outputs Actually Accepts
OpenAI strict JSON Schema explained — which keywords structured outputs and function calling actually accept, checked before you hit a 400. Free, client-side.
Try it now: OpenAI Structured Output Schema Builder — Check a JSON Schema against the structured-outputs strict subset, or build one from a sample response. Finds why the API returns a 400.
What an OpenAI Strict JSON Schema Actually Is
Without structured outputs, getting JSON back from a model call means asking nicely in the prompt and then hoping the reply parses. Most of the time it does. Occasionally the model wraps it in a sentence, adds a trailing comma, or drops a closing brace, and the parse fails in production on a request that looked identical to the thousand before it. Structured outputs — OpenAI's response_format with a json_schema and strict: true, and the equivalent constrained-decoding modes other providers ship — remove that gamble. You give the API an OpenAI strict JSON Schema, and it constrains token generation so the reply is guaranteed to conform to that schema's shape. Not “usually valid JSON” — a contract.
Making that guarantee possible is exactly why an OpenAI strict JSON Schema — the specific, restricted document a JSON Schema generator has to produce for an OpenAI function calling schema, or a response_formatJSON Schema for structured outputs — can't just emit an arbitrary JSON Schema document. Full JSON Schema allows things constrained decoding can't practically enforce at generation time: unbounded recursive $ref chains, conditional branching with if/then/else, or an additionalPropertiesleft open so the object shape is never fully pinned down. A provider that has to guarantee conformance token-by-token needs a schema that's finite and fully determined in advance — which is why every structured-outputs implementation accepts only a defined, restricted subset of JSON Schema, not the whole specification.
A Perfectly Valid Schema Can Still Get a 400
This is the detail that catches people, and it's worth stating precisely: a schema can be well-formed, fully spec-compliant JSON Schema and still fail with a 400 when you send it as a json_schema strictpayload — not because it's broken, but because it uses a keyword or configuration outside the strict subset the API accepts. The schema isn't invalid in any general sense. It's just outside what structured-outputs mode was built to enforce, and the error message rarely says that directly — you get a 400 with a keyword name in it, and it's on you to know which rule that keyword broke.
Three of the most common ways a valid schema falls outside strict mode, all in one example:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"notes": { "type": "string", "format": "regex" }
},
"required": ["name", "email"],
"additionalProperties": true
}Every line here is legal JSON Schema. None of it survives strict mode, for three separate reasons. additionalProperties: true leaves the object shape open-ended, and strict mode requires additionalProperties: false on every object so the set of possible keys is fully closed. notes is missing from required, and strict mode requires every property in properties to also appear in required — optionality has to be expressed a different way (below), not by omission. And format: "regex"is a real JSON Schema format value, but it's not one of the string formats OpenAI's strict subset recognizes, so it's rejected the same as a typo would be.
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"notes": { "type": ["string", "null"] }
},
"required": ["name", "email", "notes"],
"additionalProperties": false
}The fix for “optional” is the least obvious of the three: notes stays in required, but its type becomes a ["string", "null"] union, so the model can satisfy the requirement with an explicit nullinstead of the field being absent. That's how strict mode represents an optional field without allowing the one thing it can't decode against reliably: a key that may or may not be there at all.
Anthropic's tool-use schemas enforce their own similar-but-not-identical set of constraints, so a schema that passes OpenAI's strict check isn't automatically portable to a Claude tool definition without a second pass.
Generating a Schema From a Sample Response
The fastest starting point for any of this is rarely writing a schema from scratch — it's pointing at a real response you already have and generating a candidate schema from it. If your API returns JSON shaped like the example below, inferring the schema is mechanical: each key becomes a property, each value's JSON type becomes that property's type, and nesting is preserved as nested object/array schemas.
{
"ticket_id": "t_9f21",
"priority": "high",
"resolved": false,
"tags": ["billing", "urgent"]
}{
"type": "object",
"properties": {
"ticket_id": { "type": "string" },
"priority": { "type": "string" },
"resolved": { "type": "boolean" },
"tags": { "type": "array", "items": { "type": "string" } }
},
"required": ["ticket_id", "priority", "resolved", "tags"],
"additionalProperties": false
}Here's the point worth being explicit about: inferring a schema from a sample and checking that schema against the strict subset are two different concerns, and doing the first well doesn't guarantee the second. A generator that only reads a single sample has no way to know that a priority field might realistically also be "low" or null, and it has no way to know your downstream code actually needs an enum constraint there rather than a bare string. Generating the shape is the easy 80%; verifying that every keyword it used is one strict mode accepts — and that every optional-looking field was expressed correctly — is the part that actually decides whether the API accepts the payload. Run the generated schema through a strict-subset check before it goes anywhere near a live response_formatcall, the same way you'd run generated JSON itself through a validator before trusting it. Note also that this is a different check from validating a JSON document against a schema, which is what a JSON Schema Validator does — that confirms a piece of data matches a schema's rules; a strict-subset check confirms the schema itself is something the structured-outputs API will accept in the first place.
GenKitLab's OpenAI Structured Output Schema Builder does both halves in one pass: build a schema from a sample response the way shown above, or paste a schema you already have, and it checks the result against the strict subset directly — flagging the exact keyword, the exact nested path, and the reason it would 400, rather than leaving you to reverse-engineer the error message. It runs entirely client-side; the sample response or schema you paste is never uploaded.
Practical Use Cases
- Turning an existing API response into a structured-outputs schema. Instead of writing a
json_schemaby hand to match a response shape you already know, generate it from a real sample and check it against the strict subset in the same step. - Debugging a 400 from
response_format. When the API rejects a schema you thought was fine, checking it against the strict subset usually surfaces the exact keyword — an openadditionalProperties, a missing required field, an unsupportedformat— in seconds, instead of a binary-search through the schema by hand. - Reviewing a schema before it ships in a PR. A strict-subset check run before merge catches the same class of mistake a code reviewer would otherwise have to spot by reading the raw JSON Schema line by line.
- Deciding how to represent an optional field.Strict mode's rule — every property required, optionality expressed as a nullable type instead of an absent key — is easy to get backwards the first few times; a checker that names the rule directly is faster than re-deriving it from the docs each time.
If the schema you actually need is a TypeScript type or a runtime Zod schema rather than a structured- outputs json_schema, the same “generate an artifact from a JSON sample” approach applies — JSON to TypeScript and JSON to Zod infer from the same kind of sample response, just for a compile-time type and a runtime validator respectively, instead of an API-side schema.
Frequently asked questions
›What is a JSON Schema generator for OpenAI structured outputs?
A tool that infers a JSON Schema from a sample API response or JSON value, and — for structured outputs specifically — also checks that the inferred schema uses only keywords the strict subset accepts. Inferring the shape and confirming strict-mode compatibility are two separate steps a generator needs to cover.
›Why does OpenAI's structured outputs mode reject valid JSON Schema?
Structured outputs constrains token generation to guarantee the output matches the schema, which means the schema has to be fully determined in advance. Arbitrary JSON Schema allows things that can't be enforced that way — unbounded recursive references, if/then/else branching, open-ended additionalProperties — so the API only accepts a defined, restricted subset. A schema outside that subset can be completely valid by the general JSON Schema spec and still get a 400.
›How do I make an optional field work in a strict json_schema?
Keep it listed in required, but make its type a union that includes null — for example ["string", "null"] instead of just "string". The model then satisfies the requirement with an explicit null rather than omitting the key, which is the only form of optionality strict mode's closed-shape requirement allows.
›Does additionalProperties need to be false for structured outputs?
Yes, on every object in the schema, including nested ones. Strict mode needs the full set of possible keys pinned down in advance, and additionalProperties: true (or leaving it unset, which defaults to allowing them) leaves that set open, which is one of the more common reasons a schema that validates fine elsewhere still 400s against response_format.
›Is generating a schema from a sample response enough on its own?
No — it gets you the shape quickly, but shape inference and strict-subset compatibility are different concerns. A schema inferred from one sample can still use a format value the strict subset doesn't support, or need a field restructured as nullable rather than optional. Check the generated schema against the strict subset before using it in a live response_format call.
›Are Anthropic's tool-use schema constraints the same as OpenAI's strict subset?
No — they're similar in spirit (both need a fully-determined schema to enforce reliably) but not identical in the details, so a schema that passes OpenAI's strict check should still be checked separately before being used as an Anthropic tool definition.
Last updated