Просить JSON словами — значит однажды получить его с вежливым предисловием и сломать разбор. Схема переносит проверку с вашей стороны на сторону модели.
| Значение response_format | Что гарантируется |
|---|---|
| {"type": "json_object"} | Ответ — синтаксически валидный JSON. Форма произвольная: попросить нужные поля всё равно придётся текстом. |
| {"type": "json_schema", …} | Ответ соответствует вашей JSON Schema: и синтаксис, и набор полей, и типы. |
Первый режим — наследие, второй — то, чем стоит пользоваться. Разница не в удобстве: json_object не мешает модели вернуть валидный JSON с полем answer вместо ожидаемых пяти полей.
{
"model": "openai/gpt-5-mini",
"messages": [
{"role": "user", "content": "Разбери: ООО «Ромашка», ИНН 7707083893, Москва"}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "company",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Название без формы собственности"},
"form": {"type": "string", "enum": ["ООО", "АО", "ИП", "ПАО"]},
"inn": {"type": "string", "pattern": "^[0-9]{10}$|^[0-9]{12}$"},
"city": {"type": "string"},
"branch": {"type": ["string", "null"]}
},
"required": ["name", "form", "inn", "city", "branch"],
"additionalProperties": false
}
}
}
}additionalProperties: false обязателен на каждом объекте, включая вложенные.required должны быть перечислены все свойства. Необязательное поле выражается не отсутствием в required, а типом-объединением с null — как branch в примере выше.type, enum, properties, items, required, anyOf, $ref на определения внутри той же схемы. Рекурсия допускается, но глубину площадки режут по-разному.description модель читает и использует. Это самое дешёвое место, чтобы объяснить, что именно вы хотите в поле.inn — и вернёт, даже если ИНН в тексте не было. Поле, которого может не быть в источнике, объявляйте допускающим null и говорите об этом в промпте прямо.Возможность json_schema отмечена в capabilities модели — см. каталог. Если модель её не умеет, запрос вернёт 400: тихо снять ограничение и отдать свободный текст было бы хуже, чем отказать — разбор на вашей стороне сломается уже в проде.
Если модель умеет, а конкретная площадка — нет, маршрутизация сама выберет ту, которая умеет. Принудить явно можно через require_parameters, см. «Выбор площадки».
Схема и stream: true совместимы, но пользы от этого немного: валидный JSON получится только на последнем куске, а промежуточные разобрать нечем. Стримить структурированный ответ имеет смысл, только если вы собираете его инкрементальным парсером и показываете пользователю по мере готовности полей.
Классификация в один из пяти классов надёжнее делается не схемой, а logit_bias с max_tokens: 1 — модель физически не сможет ответить ничем, кроме разрешённых токенов. Это грубее, зато не зависит от поддержки схем и работает на любой модели.