В этой статье
Чтобы получить объект с предсказуемыми полями через OpenAI Responses API, передайте JSON Schema в text.format и включите Structured Outputs. Для успешного завершённого ответа схема задаёт структуру данных: обязательные поля, типы значений и запрет на лишние ключи. JSON mode гарантирует корректный JSON, но не соответствие вашим полям — это различие описано в документации Structured Outputs.
Дальше у приложения три задачи: проверить завершение генерации, отдельно обработать отказ модели и только затем разобрать JSON. Даже объект по схеме нужно сверить с исходными данными. Если в заметке нет даты встречи, подходящее значение — null, а не придуманная пятница.
Как передать схему в Responses API
Формат результата задаётся внутри text.format:
type: "json_schema"включает Structured Outputs;nameдаёт схеме имя;strict: trueвключает строгое соблюдение поддерживаемой схемы;schemaсодержит саму схему.
В строгом режиме корневой тип — объект, все его поля нужно перечислить в required, а additionalProperties: false запрещает дополнительные ключи. Неизвестное значение не требует пропускать поле: для даты разрешим тип ["string", "null"]. Потребитель всегда получит ключ date_text, а null сообщит, что даты нет.
Схему и примеры кода можно также обсуждать в личном ChatGPT. Если для этой отдельной работы нужен Plus, Amber Market — сервис оплаты зарубежных сервисов — предлагает месячную подписку ChatGPT Plus на своём аккаунте через менеджера. Заказ можно оплатить через СБП. Для оформления нужен аккаунт Free без действующей подписки; продление доступно после её окончания. Актуальные условия и итоговую стоимость смотрят перед оплатой. Plus и API оплачиваются отдельно: подписка не пополняет API-баланс, не меняет квоту организации и для получения JSON через API не требуется.
Полный пример на Python
Возьмём заметку: «Анна и Борис обсудят макет. Дата не назначена». Нужны три поля: тема встречи, участники и дата. Последняя должна остаться неизвестной.
Для примера нужны Python 3.10+, пакет openai и заданные пользователем переменные окружения OPENAI_API_KEY и OPENAI_MODEL. В OPENAI_MODEL укажите доступную вам модель с поддержкой Structured Outputs. SDK читает ключ из окружения; код его не выводит. Подготовка окружения разобрана в инструкции по первому запросу OpenAI API на Python.
import json
import os
from openai import OpenAI
schema = {
"type": "object",
"properties": {
"topic": {
"type": "string",
"description": "Что будут обсуждать",
},
"participants": {
"type": "array",
"items": {"type": "string"},
"description": "Указанные в заметке участники",
},
"date_text": {
"type": ["string", "null"],
"description": "Дата или формулировка даты; null, если дата не указана",
},
},
"required": ["topic", "participants", "date_text"],
"additionalProperties": False,
}
notes = "Анна и Борис обсудят макет. Дата не назначена."
client = OpenAI()
response = client.responses.create(
model=os.environ["OPENAI_MODEL"],
instructions=(
"Извлеки только данные из заметки. "
"Не придумывай отсутствующие сведения. "
"Если значение неизвестно, используй null."
),
input=notes,
text={
"format": {
"type": "json_schema",
"name": "meeting",
"strict": True,
"schema": schema,
}
},
)
if response.status != "completed":
raise RuntimeError(
f"Генерация не завершена успешно: status={response.status!r}"
)
for output_item in response.output:
if output_item.type != "message":
continue
for content_item in output_item.content:
if content_item.type == "refusal":
raise RuntimeError(
f"Модель отказалась возвращать объект: {content_item.refusal}"
)
try:
meeting = json.loads(response.output_text)
except json.JSONDecodeError as error:
raise RuntimeError("Завершённый ответ не удалось разобрать как JSON") from error
print(json.dumps(meeting, ensure_ascii=False, indent=2))
Это один запрос без инструментов. Сначала код проверяет response.status: если он отличается от completed, разбор прекращается. Затем ищет refusal в содержимом сообщений. Только после обеих проверок response.output_text попадает в json.loads. Отказ и незавершённая генерация требуют отдельной обработки: наличие схемы не превращает их в готовый объект.
Для этих заметок ожидаемый объект может выглядеть так:
{
"topic": "обсуждение макета",
"participants": [
"Анна",
"Борис"
],
"date_text": null
}
Это пример подходящего результата. Формулировка темы может отличаться, но должна означать обсуждение макета. Ключ date_text присутствует, его значение — null; Анна и Борис на месте, новых людей нет.
Где заканчиваются гарантии схемы
Схема отвечает за форму. Объект с участниками "Анна", "Борис" и датой "пятница" тоже может соответствовать нашей схеме: строка разрешена. Но такой объект противоречит исходной заметке. Инструкция просит не выдумывать дату, однако сама по себе не доказывает, что модель выполнила это требование.
Для конкретного примера можно добавить после json.loads, перед выводом результата, такие проверки:
if "date_text" not in meeting:
raise ValueError("В результате отсутствует обязательное поле date_text")
if meeting["date_text"] is not None:
raise ValueError("Дата должна быть null: во входной заметке её нет")
expected_participants = {"Анна", "Борис"}
actual_participants = meeting["participants"]
if (
set(actual_participants) != expected_participants
or len(actual_participants) != len(expected_participants)
):
raise ValueError(
"Ожидались только Анна и Борис, каждый участник по одному разу"
)
Проверка наличия date_text повторяет часть контракта схемы и делает ожидание явным. Проверка None уже проверяет факт: именно в этой заметке даты нет. Сравнение участников обнаружит пропущенного человека, новое имя или дубликат.
Эти проверки привязаны к известному входу. Они не доказывают корректность произвольной заметки и не проверяют смысл topic. Тему здесь сверяем с исходным предложением: обсуждать будут макет. Искать только подстроку «макет» недостаточно — она найдётся и в ошибочном «отмена обсуждения макета».
В приложении полезно хранить исходную заметку рядом с результатом. Тогда спорное значение можно сверить с текстом, из которого оно извлечено.
Как добавить новое поле
Новое поле добавляется и в properties, и в required. Например, для места встречи в properties понадобится:
"location": {
"type": ["string", "null"],
"description": "Место встречи или null, если оно неизвестно",
}
А список обязательных полей станет таким:
"required": [
"topic",
"participants",
"date_text",
"location",
]
Поле остаётся обязательным, даже если его значение неизвестно. Для этого случая заранее разрешён null. Если описать location в properties, но не включить в required, схема не выполнит требования строгого режима и API её отклонит.
Если API отклоняет схему
Проверьте три места:
- Выбранная через
OPENAI_MODELмодель поддерживает Structured Outputs. - Схема использует поддерживаемое подмножество JSON Schema: в частности, все поля обязательны и дополнительные ключи запрещены.
- Формат находится в
text.formatвызова Responses API. Параметрresponse_formatиз примеров Chat Completions сюда переносить нельзя.
Одинаковый повтор не исправит неподдерживаемую схему. Прочитайте ошибку API, поправьте причину и отправьте исправленный запрос. Его результат проходит тот же путь: статус, отказ, разбор JSON и сверка с входными данными.
Если JSON нужен вручную в обычном чате, используйте инструкцию по JSON для программирования в ChatGPT. Здесь контракт задаёт программа через Responses API — вместе с обработкой случаев, когда готового объекта нет.
Источники
- Structured Outputs — OpenAI APIOpenAI
- Подписки ChatGPT — Amber MarketAmber Market
- What is ChatGPT Plus?OpenAI