Учебник

Как 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.

Следующий шаг: возьмите самый хрупкий эндпоинт и сделайте «одна схема, один запрос, одна локальная проверка». Сначала утихомирьте этот путь, потом скопируйте приём.