Skip to main content
Set response_format and the model’s reply is parsed, pruned to your schema’s own keys, validated against it, repaired at most once, and returned as a json output item — or refused with a 422 that names the paths that did not match.
The output item type is json and the value is under the json key. output is still an array — a run can produce prose and a structured object.

Three formats

You may send the short string form ("response_format": "json_schema") or the object form. The object form takes name (up to 64 characters), schema (required for json_schema) and strict.
strict is accepted only as true. There is no mode in which a schema is requested and not enforced. A caller told their schema was honoured when it was not is the single outcome this whole feature exists to prevent, and a flag that turns validation off is that outcome with a name.

The schema subset

The validator is a closed keyword set, not a full JSON Schema engine. Accepting a keyword and then ignoring it is how an API quietly returns something other than what was asked for. Accepted:
Types: object, array, string, number, integer, boolean, null. Formats: email, date, date-time, uri, uuid. Refused, each with a 400 naming the keyword and the reason: Bounds: 16 KB of schema, 6 levels deep, 200 nodes, 100 properties per object, 200 enum values. Property names match [A-Za-z_][A-Za-z0-9_-]{0,63} — the characters that are the evidence-path grammar (. and []) cannot also appear in a name, or an entry for a.b would be indistinguishable from one for b inside a.

Two deliberate departures from JSON Schema

In JSON Schema, format is a hint a validator may ignore. Here it is checked. A caller who writes "format": "email" is telling us what they will do with the value, and handing them "n/a" because the spec permits it would be the wrong kind of correct.
JSON Schema counts the key as present. {"full_name": null} is not an answer, it is the absence of one wearing a key.If you genuinely want “present, possibly null”, write "type": ["string", "null"]. The subset supports it and it then passes.

additionalProperties prunes rather than fails

It defaults to false, and the effect is to remove keys you did not ask for before validation runs. A model volunteering an extra field is the commonest form of plausible-looking noise, and neither silently returning it nor failing the whole run over it is the right answer.

Cost is knowable before the run starts

One repair pass by default; two at the absolute most. So one structured run costs at most 1 + max_repairs model calls, and a bad schema cannot turn into an open-ended spend. There are no unlimited repair loops. A model that produced the wrong shape twice, given the exact failing paths, is not going to produce the right one on the third try — and each attempt is a full turn, with knowledge, memory and tools.

When it fails

1

A schema this runtime cannot enforce is a 400, before any spend

The schema is checked when the request is parsed — before authorization, before the idempotency claim, before a single model call. So an unsupported keyword costs you nothing.
2

Output that will not validate is a 422

At most five failing paths are named — enough to fix a prompt, few enough that a wildly wrong answer does not produce a paragraph.
A 422 names the paths and never the values. You do not get the invalid object back, not even “just to see”.Handing it over would be unvalidated output delivered through the error channel, and an integration that reads it there has silently accepted exactly what the validation exists to refuse.
Rejected output does not reach the transcript either. In a conversation, only the validated object is ever written.

Streaming a structured run

Fragments arrive as response.output_json.delta rather than response.output_text.delta. They are still advisory — read the validated object off response.completed. A partial JSON string is not JSON.

Reports

POST /reports accepts "format": "json" with a schema and honours it the same way. See Reports.