Skip to main content

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

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)

The raw wire spellings, if you are not using an SDK helper:

APIField
Chat Completionsresponse_format: {"type": "json_schema", "json_schema": {"name": …, "schema": …, "strict": true}}
Responsestext: {"format": {"type": "json_schema", "name": …, "schema": …, "strict": true}}
Messagesoutput_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 mode 400.
  • muse-spark and muse-spark-1-1 (Meta): response_format is refused with 400 param_not_supported until Meta's own spelling is verified; prompt for JSON and validate, or use tool calling.
  • Gemini models: a schema and tools in 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.