Structured output
Ask for a JSON schema and the model's answer is JSON matching it. Every API has its own spelling for the request, and all three are honored: response_format on Chat Completions, text.format on Responses, output_config.format on Messages.
Works with
Every model that can constrain its output. This is the one parameter that is refused rather than dropped: a model that cannot constrain its output returns 400 param_not_supported, because prose handed to a caller who is about to json.loads() it fails later and less legibly than an error here. The per-model column is on the capability matrix.
Request
- Chat Completions
- Responses
- Messages
- Chat Completions
- Responses
- Messages
from pydantic import BaseModel
class City(BaseModel):
name: str
country: str
population: int
completion = client.chat.completions.parse(
model="mindshub_air",
messages=[{"role": "user", "content": "Describe Lisbon."}],
response_format=City,
)
city = completion.choices[0].message.parsed
print(city.population)
from pydantic import BaseModel
class City(BaseModel):
name: str
country: str
population: int
response = client.responses.parse(
model="mindshub_air",
input="Describe Lisbon.",
text_format=City,
)
print(response.output_parsed.population)
message = client.messages.create(
model="mindshub_air",
max_tokens=1024,
messages=[{"role": "user", "content": "Describe Lisbon."}],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"country": {"type": "string"},
"population": {"type": "integer"},
},
"required": ["name", "country", "population"],
"additionalProperties": False,
},
}
},
)
import json
city = json.loads(message.content[0].text)
print(city["population"])
import { zodResponseFormat } from "openai/helpers/zod";
import { z } from "zod";
const City = z.object({ name: z.string(), country: z.string(), population: z.number() });
const completion = await client.chat.completions.parse({
model: "mindshub_air",
messages: [{ role: "user", content: "Describe Lisbon." }],
response_format: zodResponseFormat(City, "city"),
});
console.log(completion.choices[0].message.parsed?.population);
import { zodTextFormat } from "openai/helpers/zod";
import { z } from "zod";
const City = z.object({ name: z.string(), country: z.string(), population: z.number() });
const response = await client.responses.parse({
model: "mindshub_air",
input: "Describe Lisbon.",
text: { format: zodTextFormat(City, "city") },
});
console.log(response.output_parsed?.population);
const message = await client.messages.create({
model: "mindshub_air",
max_tokens: 1024,
messages: [{ role: "user", content: "Describe Lisbon." }],
output_config: {
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
name: { type: "string" },
country: { type: "string" },
population: { type: "integer" },
},
required: ["name", "country", "population"],
additionalProperties: false,
},
},
},
});
const city = JSON.parse((message.content[0] as { text: string }).text);
console.log(city.population);
The raw wire spellings, if you are not using an SDK helper:
| API | Field |
|---|---|
| Chat Completions | response_format: {"type": "json_schema", "json_schema": {"name": …, "schema": …, "strict": true}} |
| Responses | text: {"format": {"type": "json_schema", "name": …, "schema": …, "strict": true}} |
| Messages | output_config: {"format": {"type": "json_schema", "schema": …}} |
Schemas are forwarded to the target model as you wrote them; nothing is silently rewritten to make a schema fit. Where a provider's schema dialect is narrower than JSON Schema, the request is adapted at that provider's edge, and a schema the provider rejects returns that provider's own 400 naming the keyword at fault.
Response
The answer arrives as the API's normal text: message.content on Chat Completions, output_text on Responses, a text block on Messages. It parses as JSON matching your schema. Responses echoes the text.format you sent back on the response object, and responses.parse() / chat.completions.parse() populate their parsed fields.
Streaming
Structured output streams like any other text; the JSON arrives in fragments and is complete when the turn ends. Parse after the final frame. A schema request that a model can't honor fails before the stream opens, as a plain 400.
Schema-less JSON mode
{"type": "json_object"} (Chat Completions and Responses) asks for valid JSON without a schema. It is honored on every transport except the Claude family, where it is a 400: Anthropic's API has no schema-less mode and no faithful way to fake one (its schemas mandate additionalProperties: false, so the obvious stand-in constrains the answer to the literal empty object). Send a schema instead. Messages has no spelling for JSON mode at all.
Per-model differences
- Claude family (
opus,sonnet,fable,haiku): schema output yes, JSON mode400. muse-sparkandmuse-spark-1-1(Meta):response_formatis refused with400 param_not_supporteduntil Meta's own spelling is verified; prompt for JSON and validate, or use tool calling.- Gemini models: a schema and
toolsin the same request is a provider-side conflict and fails upstream. - Fireworks-hosted models (
deepseek,qwen,glm,kimi): both schema and JSON mode work.
The full per-model column is on the capability matrix.
Errors you can hit
400 param_not_supported when the model cannot constrain its output. The error's param names the field in your own API's spelling (response_format, text.format, or output_config.format). A schema the provider rejects returns the provider's own 400. See Errors.