Guides
Structured outputs
Request JSON objects or responses that follow a JSON schema
Sometimes you want the model to return JSON, not prose. Two shapes — JSON mode (valid JSON, any shape) and JSON Schema (valid JSON matching a schema).
JSON mode
from openai import OpenAI
import json
client = OpenAI(
api_key="sk-mel-<YOUR_API_KEY>",
base_url="https://api.melious.ai/v1",
)
response = client.chat.completions.create(
model="<MODEL_ID>",
messages=[
{"role": "system", "content": "Reply with JSON only."},
{"role": "user", "content": "Pick three Hanseatic cities. Include name and modern country."},
],
response_format={"type": "json_object"},
)
data = json.loads(response.choices[0].message.content)
print(data)The system prompt matters — in JSON mode, the model needs to be told to produce JSON. Otherwise it may return an empty object or a weird shape because you didn't tell it what you wanted. "Reply with JSON only" is the minimum viable instruction.
JSON Schema
Stricter — the response is constrained to match your schema:
schema = {
"type": "object",
"properties": {
"cities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"country": {"type": "string"},
"founded_year": {"type": "integer"},
},
"required": ["name", "country", "founded_year"],
"additionalProperties": False,
},
},
},
"required": ["cities"],
"additionalProperties": False,
}
response = client.chat.completions.create(
model="<MODEL_ID>",
messages=[
{"role": "user", "content": "Pick three Hanseatic cities with founding years."},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "cities_list",
"schema": schema,
"strict": True,
},
},
)Models with _meta.capabilities.json_schema == true constrain generation to your schema. Every other model rejects a json_schema request with 400 before anything runs — switch to json_object for those, or pick a model that has the capability.
What happens when the model produces not-quite-valid JSON
On non-streaming requests with json_object or json_schema, we run the model's text through a JSON repairer before returning it: trailing commas, unquoted keys, markdown fences, and a missing closing brace get fixed. Two honest limits: streams are not repaired, and when nothing is recoverable you get the original text or an empty string — never an error. Wrap your json.loads in a try and check finish_reason == "length" when it fails.
Which models support what
json_objectmode — almost every chat model. Check_meta.capabilities.structured_output.json_schema(strict) — subset of models. Check_meta.capabilities.json_schema.
Asking for a format a model doesn't support returns a 400 with type: invalid_request_error, code: model_not_capable.
Responses shape
The Responses API takes the schema in text.format, in the flat shape the OpenAI SDK's client.responses.parse() sends:
response = client.responses.create(
model="<MODEL_ID>",
input="Pick three Hanseatic cities with founding years.",
text={
"format": {
"type": "json_schema",
"name": "cities_list",
"schema": schema,
"strict": True,
}
},
)
data = json.loads(response.output_text)client.responses.parse() works too — it sends this shape and parses the result for you. We forward name, schema, strict, and description as you sent them, and we still accept the nested {"type": "json_schema", "json_schema": {...}} shape from Chat Completions. {"format": {"type": "json_object"}} is JSON mode. The capability rule is the same as above: the model needs _meta.capabilities.json_schema, or you get the 400 before a stream starts.
Anthropic shape
The Messages API takes the schema in output_config.format. We translate it to the same strict json_schema mode as Chat Completions, so the model needs _meta.capabilities.json_schema:
response = client.messages.create(
model="<MODEL_ID>",
max_tokens=512,
output_config={"format": {"type": "json_schema", "schema": schema}},
messages=[{"role": "user", "content": "Pick three Hanseatic cities..."}],
)The JSON comes back in the first text block. Only type: "json_schema" does anything; other format types are ignored. If you'd rather not depend on json_schema support, the tool-call pattern works on every model that calls tools:
client.messages.create(
model="claude-sonnet-4",
max_tokens=512,
tools=[{
"name": "record_cities",
"description": "Record a list of Hanseatic cities",
"input_schema": schema,
}],
tool_choice={"type": "tool", "name": "record_cities"},
messages=[{"role": "user", "content": "Pick three Hanseatic cities..."}],
)Force the model to call record_cities — the input field on the resulting tool_use block is your structured output.
Patterns that work
A few things we've seen:
- Use structured output for extraction, not generation. "Pull fields X/Y/Z from this text" works well; "generate creative JSON with arbitrary keys" works less well.
additionalProperties: falsematters. Without it, models sometimes invent extra keys.- Keep required fields tight. Every required field the model skips forces a regeneration; optional fields are free.
- When a schema includes enums, list the options in the description. The model reads descriptions more attentively than raw schema enum lists.
Gotchas
max_tokenstruncation is the most common failure mode. You may get back a trimmed but valid object, or unparseable text — there is no error for this. Either way, checkfinish_reason == "length"and raisemax_tokensif needed.- Nested arrays of arrays confuse some models. Flatten to keyed objects where you can.
Related
Chat completions for the full response_format field • Responses for text.format • Tool calling for the tool-based extraction pattern on Messages.