Учебник
Как Gemini API выводит JSON: Structured Output и JSON Schema
Хватит просить модель «вернуть только JSON». Зафиксируйте форму через Structured Output, ограничьте поля JSON Schema — тогда downstream-код сможет стабильно разбирать ответ.
Вы просите у Gemini JSON и получаете преамбулу, пропущенную запятую или более «дружелюбные» имена полей. Просьба в промпте «выводи только JSON» снижает шансы; это не контракт. Structured Output переносит контракт в декодирование: объявите MIME-тип, приложите схему — и модель выдаёт токены этой формы. После гайда вы выберете режим JSON или Schema, напишете рабочий запрос и всё равно проверите ответ локально.
Зачем нужен структурированный вывод
Как только ниже по пайплайну вызывают JSON.parse, сбой — уже не ошибка копипаста: останавливается весь конвейер. Классификаторам нужны фиксированные enum, экстракторам — стабильные ключи, вызовам инструментов — объекты параметров. Свободный текст дорог: один битый JSON — это ретраи, логи или ещё один клик пользователя.
Тише ломается так: «парсится, но форма не та». Вы ждали, что items будет массивом, а пришёл объект; ждали, что score будет числом, а пришло "0.9". Код читает undefined, и баг взрывается гораздо позже. Structured Output чинит форму, не истину: вы получаете законный JSON по схеме, а не гарантированно верную категорию. В проде по-прежнему нужны бизнес-проверки — парсинг просто становится тише.
Официальные заметки о возможностях — в документации Gemini Structured output. Google также анонсировал более широкий набор ключевых слов JSON Schema и порядок свойств; читайте обновление Structured Outputs. Перед тем как писать схему, пробегитесь по Understanding JSON Schema, чтобы не путать «слова, которые разрешает спецификация» и «слова, которые эта модель реально принуждает».
Режим JSON и режим Schema
Два переключателя. Первый обещает только: «эта строка распарсится как JSON». Второй: «этот JSON совпадает со схемой, которую вы объявили». Если код читает именованные поля — берите второй. Первый — только для разведочного извлечения, где модель ещё и ключи придумывает.
| Уровень | Что вы настраиваете | Что вы реально получаете |
|---|---|---|
| Режим JSON | Только responseMimeType: application/json |
Обычно валидный JSON; имена и вложенность по-прежнему выбирает модель |
| Режим Schema | MIME-тип + responseSchemaилиresponseJsonSchema |
Форма, типы, обязательные поля и enum следуют схеме |
С одним режимом JSON документация всё ещё считает это сильным намёком с небольшим риском кривого вывода. Чтобы приблизиться к «всегда парсится как объект», отправьте и схему. Схема входит в входные токены, поэтому не копируйте то же описание в промпт: дублирование бьёт по качеству и квоте.
responseSchema или responseJsonSchema
responseSchema использует подмножество схемы в стиле OpenAPI 3.0. Имена типов в REST часто в верхнем регистре, например OBJECT, STRING. Подходит для плоских объектов, классификации enum и фиксации порядка ключей через propertyOrdering. Не понимает $ref / $defs, поэтому рекурсивные деревья и общие определения приходится инлайнить — и размер скоро взрывается.
responseJsonSchema рассчитан на Gemini 2.5 и новее и говорит ближе к стандартному JSON Schema, покрывая anyOf, $ref, minimum / maximum, additionalProperties, type: null, prefixItems и прочее. Генерация схемы из Pydantic или Zod снижает трение. Более новые модели сохраняют порядок ключей как объявлено — удобно для diff логов и golden-тестов.
Три правила на глаз. Плоская классификация/извлечение: подойдёт любое поле. Рекурсия, общие определения или union: берите responseJsonSchema. Если нужно зафиксировать порядок полей, проверьте, что эндпоинт всё ещё чтит propertyOrdering; не считайте, что каждая поверхность API ведёт себя одинаково.
Как писать схему
Опишите объект, который вы реально будете читать, а не полную модель мира. Каждое лишнее необязательное поле — ещё один шанс наполнить мусором. Обязательные имена — в required, enum — в enum, числовые границы — в minimum / maximum. Добавить description у свойств часто стабильнее, чем объяснять их снова в промпте: ограничение едет вместе с декодированием.
Схема ниже моделирует классификацию тикетов: category — одно из трёх значений, priority — целое, summary — строка. Вот чем должен владеть режим Schema — формой, а не тем, срочный ли тикет на самом деле.
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "bug", "feature"],
"description": "Ticket category"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5
},
"summary": {
"type": "string"
}
},
"required": ["category", "priority", "summary"],
"additionalProperties": false
}
Для массивов элементы задают через items. Для кортежеподобных списков фиксированной длины смотрите prefixItems на пути JSON Schema. «Не в required» — это не nullable: явно разрешите null, иначе модель может опустить ключ, а код всё ещё считает, что obj.field всегда есть.
Вложенные объекты инланьте. Тянитесь к $defs + $ref только когда та же структура появляется в третий раз или узел дерева ссылается на себя. Ранняя абстракция делает отказы нечитаемыми: вы отлаживаете развёрнутый ком, когда сервер отвергает схему.
Python и JavaScript на практике
Фрагменты используют обычные формы официального SDK. Подставьте id модели на тот SKU 2.5 / новее, который у проекта реально есть — не считайте имя из примера замороженным продакшен-пином.
Python: MIME-тип + JSON Schema
from google import genai
client = genai.Client()
schema = {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "bug", "feature"]},
"priority": {"type": "integer"},
"summary": {"type": "string"},
},
"required": ["category", "priority", "summary"],
}
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Classify this ticket: invoice PDF cannot be downloaded.",
config={
"response_mime_type": "application/json",
"response_json_schema": schema,
},
)
print(response.text)
Если команда уже моделирует через Pydantic, передайте Model.model_json_schema() в response_json_schema, затем model_validate_json(response.text) для второй локальной проверки. Слой один — форма API; слой два — ваша система типов отвергает значения, которые выглядят законно, но бессмысленны, вроде priority 99.
JavaScript: generationConfig
const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: "Classify this ticket: invoice PDF cannot be downloaded.",
config: {
responseMimeType: "application/json",
responseJsonSchema: {
type: "object",
properties: {
category: { type: "string", enum: ["billing", "bug", "feature"] },
priority: { type: "integer" },
summary: { type: "string" },
},
required: ["category", "priority", "summary"],
},
},
});
const data = JSON.parse(response.text);
REST-вызовы кладут те же поля в generationConfig. OpenAPI-стиль responseSchema на части эндпоинтов всё ещё использует типы в верхнем регистре — не смешивайте это с нижним регистром object в JSON Schema. Сразу парсите через JSON.parse; не выскребайте огороженный блок кода регуляркой. Вы объявили JSON MIME — считайте всё тело JSON.
Типичные ловушки
- Описать структуру дважды — в промпте и в схеме — заставляет модель качаться между двумя описаниями.
- Раздутые схемы: глубокая вложенность, глубокий
$refили широкийanyOfмогут отвергнуть или слабо принудить. Сначала крошечный объект, потом растите. - Не отдавайте истину схеме. Enum ограничивает множество, но не мешает выбрать неверно. На критических путях — выборка или правила.
- Локальные типы расходятся со схемой запроса. Вы меняете Pydantic/Zod и забываете схему в payload; в проде тихо появляются лишние ключи или пропадают старые.
- Не тестируйте только счастливый путь. Добавьте пустые массивы, nullable-поля, длинные строки и незаконные enum (их должны блокировать).
Ещё инженерный момент: логи не должны хранить только response.text. Записывайте id модели, хеш схемы и версию промпта. Поломки Structured Output обычно «сменились дефолты» или «крошечная правка схемы отвергнута». Без этих трёх вы будете клясться, что вчера работало.
В прод: проверить, сравнить, повторить
Тело API — недоверенные байты. Распарсите, проверьте той же схемой, затем смапьте на внутренние типы. При сбое логируйте сырой текст (секреты замаскируйте) и выбирайте retry или деградацию. Не ретрайте с совсем другой схемой — не отличите шум модели от плывущего контракта.
Пока отлаживаете, вставляйте примеры в инструменты на сайте. Форматирование JSON, чтобы увидеть вложенность, Проверка JSON, чтобы поймать синтаксис, затем бросьте контракт в JSON Schema и проверьте, проходит ли экземпляр. Когда поля появляются или исчезают, JSON Diff два ответа, а не сканируйте логи глазами.
Проектируя пайплайн извлечения, напишите вручную один «идеальный вывод», выведите типы инструментом Schema и вставьте эту схему обратно в запрос Gemini. Контракт живёт в одном месте: в документе, который можно тестировать, а не в устной договорённости в истории чата.
Частые вопросы FAQ
Хватит ли одного application/json?
Для разведки — да. Как только код читает фиксированные поля, отправьте и схему. Иначе вы получите прозу в форме JSON, а не API.
Может ли Structured Output заменить function calling?
Нет. Function calling даёт модели выбрать инструмент и заполнить аргументы. Structured Output ограничивает форму этого ответа. Нужно выполнить код или стукнуть во внешний API? Инструменты. Нужен один типизированный блоб? Structured Output — и без лишнего круга.
Почему схему отвергли?
Обычно неподдерживаемые ключевые слова на этом эндпоинте, слишком глубокая рекурсия или смешение диалектов responseSchema и responseJsonSchema. Сожмите до одного объекта с тремя полями, докажите, что работает, потом добавляйте.
Вывод всегда верный?
Нет. Форма может быть валидной, факты — нет. Деньги, почта и категории тикетов по-прежнему нуждаются в правилах или человеческой выборке.
Итог и следующие шаги
Надёжный JSON не рождается из более длинного «пожалуйста, только JSON». Он рождается из MIME-типа плюс схемы. Для плоских задач responseSchemaилиresponseJsonSchema годится; для $ref, union или схем из Pydantic/Zod берите последнее. Всё равно проверяйте локально и превращайте сбои в регрессионные тесты через format, Schema и Diff.
Следующий шаг: возьмите самый хрупкий эндпоинт и сделайте «одна схема, один запрос, одна локальная проверка». Сначала утихомирьте этот путь, потом скопируйте приём.