---
title: Structured output
description: An answer to a named JSON Schema — what format the API takes, what strict means, how it is spelled on the three dialects, and why support is proven separately for every model.
keywords: structured output, json_schema, response_format, strict, schema, refusal
group: gateway
---

## Quick {#quick keywords="response_format, json_schema, schema, example"}

Add `response_format` and the answer comes back to your schema instead of as free text.

```json title=Request
{
  "model": "<model>",
  "max_tokens": 256,
  "messages": [{ "role": "user", "content": "Parse this address: 15 Baumana St, Kazan." }],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "postal_address",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": [
          { "name": "city", "type": "string", "description": "The city." },
          { "name": "street", "type": "string", "description": "The street." },
          { "name": "house", "type": "string", "description": "The house number." }
        ]
      }
    }
  }
}
```

```python title=Python
import json
import os

from openai import OpenAI

client = OpenAI(base_url="https://api.kumorouter.com/v1", api_key=os.environ["KUMO_API_KEY"])

answer = client.chat.completions.create(
    model="<model>",
    max_tokens=256,
    messages=[{"role": "user", "content": "Parse this address: 15 Baumana St, Kazan."}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "postal_address",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": [
                    {"name": "city", "type": "string", "description": "The city."},
                    {"name": "street", "type": "string", "description": "The street."},
                    {"name": "house", "type": "string", "description": "The house number."},
                ],
            },
        },
    },
)
address = json.loads(answer.choices[0].message.content)
```

## What a schema looks like {#schema keywords="profile, properties, types, field list"}

The schema travels under a name — carried to the supplier as this protocol carries it — and follows the published profile.

| Member | What it is |
| --- | --- |
| `name` | The schema's name: letters, digits, hyphen and underscore, up to 64 characters. |
| `strict` | Required, and it is `true`. |
| `schema` | The schema itself: the root is an object, and its fields are listed. |

`properties` is a **list of named fields**, not a map. A field carries `name`, `type` and an optional `description`, and there are five types: `boolean`, `integer`, `null`, `number`, `string`. A list rather than a map, because every object on this contract closes itself and a map of arbitrary names cannot.

The member has no other kind of format. A bare "just JSON" mode is not offered here: it promises valid JSON and nothing about its shape, and that is a downgrade this platform will not make silently.

## What strict means {#strict keywords="strict, requirement, refusal, semantics"}

`strict: true` means the supplier must **enforce** the schema rather than be encouraged toward it. The member takes no other value: `strict: false` is refused by name, because a schema that is not enforced is a suggestion rather than a contract, and serving two different guarantees under one name would be worse than refusing.

Strictness is proven separately from structured output itself. A model-and-channel pair that has not proven strict semantics refuses such a request **before** anything upstream is contacted, rather than trying and downgrading.

## On the other dialects {#dialects keywords="responses, text.format, messages, tools"}

On Responses the same requirement lives in `text.format` and carries four members at once: `type`, `name`, `strict` and `schema`.

```json title=Responses
{
  "text": {
    "format": {
      "type": "json_schema",
      "name": "postal_address",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": [{ "name": "city", "type": "string" }]
      }
    }
  }
}
```

The Messages dialect has no structured-output member at all. When you want a parsed object there, declare a tool: its `input_schema` is your own schema, and the call's arguments arrive as an object.

> [How to declare a tool →](/en/tools)

## In a stream {#streaming keywords="streaming, deltas, schema, chunks"}

A structured answer streams in the same deltas as ordinary text: the JSON arrives in fragments and is parseable only whole. Parse after the terminal frame, not on the way.

Streaming a structured answer is a proven capability of its own, separate from streaming and from structured output alike: the catalog keeps a `structured_output_streaming_status` cell for the pair.

> [The terminal-frame rule →](/en/streaming)

## When the model refuses {#refusal keywords="refusal, content null"}

A model may refuse to answer. Then `content` arrives as `null` and `refusal` sits beside it, in the model's own words. That is **an answer, not an error**: the request was served, the supplier accounted for it, and it is settled like any other. No error envelope arrives, the status stays successful, and the client has to read `refusal` for itself.

On Responses a refusal arrives as an output part of kind `refusal`, and in a stream as the `response.refusal.delta` event.

## A schema is proven per model {#proof keywords="catalog, structured_output_status, strict_semantics_status"}

:::note
Structured output is a proven capability of an exact model and channel. The catalog keeps four cells for it: `structured_output_status`, `structured_output_mode`, `strict_semantics_status` and `structured_output_streaming_status`. Each of the three status cells takes `proven`, `unproven` or `unsupported`, and "unproven" never means "probably supported": a request to an unproven pair is refused before anything upstream is contacted. What a given model has proven is on its card in the catalog.
:::

## Next {#next keywords="tools, streaming, errors, catalog"}

:::cards
- [Tools](/en/tools) — when you want a call rather than an answer to a schema.
- [Streaming](/en/streaming) — how a structured answer arrives in fragments.
- [Errors](/en/errors) — the refusal envelope and the status codes.
- [Chat Completions](/en/chat-completions) — the request members in full.
- [Model catalog](/en/models) — what each model has proven.
:::
