In this section · 03 Schemas for validators and modelsStructured output
03 · Schemas for validators and models
Structured output for OpenAI, Claude and Gemini
A schema each provider accepts, what it had to change to accept it, and the request it goes in.
A language model asked for JSON will usually give you JSON, and now and then give you something close to it that your parser rejects. Structured output closes that gap: you send a schema with the request, and the provider guarantees the answer fits it. It sits in the Generate section beside the languages, because a schema for a model is written from a sample in exactly the way a type is.
Writing that schema is where the friction is, because each provider accepts a different subset of JSON Schema and rejects the rest. Generate starts from a sample of the answer you want and writes the schema each provider will take, then says what it had to change to get there. The sample on this page is the list of tasks a model might pull out of a meeting’s notes. The first task has only a title; the second also has a due date, so due is optional, and every provider treats that differently.
OpenAI
Input
tasks.json
[
{ "title": "Draft the brief" },
{ "title": "Book a room", "due": "Friday" }
]Do
- Set From to JSON, and under Structured output choose OpenAI.
- In Options, under OpenAI, set Output to Schema.
- Paste the input into the source pane, and type
Tasksinto Root name, since a paste has no file name to take it from.
Result
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/$defs/Task"
}
}
},
"required": [
"items"
],
"additionalProperties": false,
"$defs": {
"Task": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"due": {
"type": [
"string",
"null"
]
}
},
"required": [
"title",
"due"
],
"additionalProperties": false
}
}
}
Note: OpenAI wrapped the root at $ as the required items object.
Note: OpenAI made 1 optional field nullable at $[].due.OpenAI’s strict mode has two rules that change this schema, and the two lines under it are the report of each. A schema’s root must be an object, so the list is wrapped in one with a single required property, items: the model will answer with an object, and your code reads its items. And every property must be listed as required, so the optional due is required and allowed to be null instead. A task with no due date comes back with "due": null rather than without the key.
On the page the report sits under the code, and a warning is coloured. The block above has no colour, so each line starts with the word a screen reader hears in its place.
Claude
Input
tasks.json
[
{ "title": "Draft the brief" },
{ "title": "Book a room", "due": "Friday" }
]Do
- Set From to JSON, and under Structured output choose Claude.
- In Options, under Claude, set Output to Schema, and leave Make all fields required unticked.
- Paste the input into the source pane, and type
Tasksinto Root name, since a paste has no file name to take it from.
Result
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/$defs/Task"
}
}
},
"required": [
"items"
],
"additionalProperties": false,
"$defs": {
"Task": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"due": {
"type": "string"
}
},
"required": [
"title"
],
"additionalProperties": false
}
}
}
Note: Claude wrapped the root at $ as the required items object.Claude also wants an object at the root, so the list is wrapped the same way. Unlike OpenAI, it accepts an optional property, so due stays out of required and keeps its plain string type, and a task without a date comes back without the key.
Claude does cap how many optional properties a single request may carry, at 24 across the whole schema. A large sample can pass that, and the report then carries a warning saying how many it found. Tick Make all fields required and the schema is written the OpenAI way instead: every property required, the optional ones allowed to be null. It is also the setting to choose when your code would rather check for a null than for a missing key.
Gemini
Input
tasks.json
[
{ "title": "Draft the brief" },
{ "title": "Book a room", "due": "Friday" }
]Do
- Set From to JSON, and under Structured output choose Gemini.
- In Options, under Gemini, set Output to Schema.
- Paste the input into the source pane, and type
Tasksinto Root name, since a paste has no file name to take it from.
Result
{
"type": "array",
"items": {
"$ref": "#/$defs/Task"
},
"$defs": {
"Task": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"due": {
"type": "string"
}
},
"required": [
"title"
],
"additionalProperties": false
}
}
}Gemini takes a list at the root as it is, and an optional property as it is, so its schema is the plain one and its report is empty. That is the one case where the list under the code does not appear at all: nothing was changed, so there is nothing to say.
Schema or request fragment
Output has two choices. Schema is the schema alone, for code that already builds its own request. Request fragment wraps it in the part of the request body that carries it, ready to merge into your call:
Input
tasks.json
[
{ "title": "Draft the brief" },
{ "title": "Book a room", "due": "Friday" }
]Do
- Set From to JSON, and under Structured output choose Gemini.
- In Options, under Gemini, set Output to Request fragment and API to generateContent.
- Paste the input into the source pane, and type
Tasksinto Root name, since a paste has no file name to take it from.
Result
{
"generationConfig": {
"responseMimeType": "application/json",
"responseJsonSchema": {
"type": "array",
"items": {
"$ref": "#/$defs/Task"
},
"$defs": {
"Task": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"due": {
"type": "string"
}
},
"required": [
"title"
],
"additionalProperties": false
}
}
}
}
}Gemini’s fragment sets the response type to JSON and passes the schema beside it, and API chooses between the two Gemini endpoints that take one. OpenAI’s puts the schema under a named json_schema format with strict mode on, and its API choice covers the Responses API and Chat Completions. Claude’s Use choice sends it either as the answer’s format or as the input of a strict tool. Those extra choices, and a Name where the request has one, only appear while Output is Request fragment.
Nothing is sent anywhere
Generate writes a schema. It never calls OpenAI, Anthropic or Google, and it holds no key for any of them, so there is no account to set up and no usage to pay for while you try one shape after another. Sending the request is your code’s job, with your own key, from wherever your code runs. The sample you paste stays in this browser tab, which matters when the sample is a real answer holding real data.
A provider schema is a stricter relative of the JSON Schema target. For a schema to validate files with rather than to steer a model, see A JSON Schema, and checking the next file. Open Generate to start from a sample of your own, or Open the editor to shape the sample first.