output_schema
Как спроектировать JSON-схему ответа — типы, вложенность, массивы, описания как подсказки модели. Самый частый источник ошибок у новичков.
output_schema — единственный рычаг, которым вы управляете формой ответа
/v1/process. Она обязательна везде, где вызывается
модель: prompt без непустой схемы не существует (PROMPT_REQUIRES_SCHEMA),
а result всегда приходит JSON-объектом.
Модель получает вашу схему вместе с промптом и старается вернуть JSON,
который ей соответствует. Это не строгий валидатор в духе Ajv/JSON Schema
Draft — это инструкция модели, и разница между «инструкцией» и
«валидатором» объясняет большинство неожиданностей новичков (см. раздел
«Чего схема не гарантирует» ниже).
Как передавать схему — самая частая ошибка на старте
Эндпоинт один, но форма передачи output_schema зависит от типа тела
запроса:
| Вход | Тело запроса | output_schema |
|---|---|---|
| Файл | multipart/form-data | строка с JSON внутри |
| Готовый текст | application/json | обычный вложенный объект |
# multipart — output_schema передается ОДНОЙ строкой (Form-поле):
curl -X POST https://api.docinspect.ru/v1/process \
-H "X-API-Key: $API_KEY" \
-F "file=@invoice.pdf" \
-F "prompt=Извлеки номер и сумму счета" \
-F 'output_schema={"type":"object","properties":{"number":{"type":"string"},"amount":{"type":"number"}}}'// JSON-тело — output_schema уже настоящий объект, БЕЗ кавычек вокруг:
{
"prompt": "Извлеки номер и сумму счета",
"text": "...",
"output_schema": { "type": "object", "properties": { "number": { "type": "string" }, "amount": { "type": "number" } } }
}То же правило действует для остальных составных полей — include, split,
match: в multipart это JSON-строки, в JSON-теле — обычные массивы и
объекты. Невалидный JSON в form-поле дает 422 FIELD_NOT_JSON, а не
молчаливо проигнорированную схему.
Типичный баг на JS-стороне — положить объект прямо в FormData без
JSON.stringify:
// ❌ FormData сериализует объект в "[object Object]" — сервер получит
// невалидный JSON и просто проигнорирует схему (result придет без формы)
formData.append("output_schema", { type: "object", properties: {} });
// ✅
formData.append("output_schema", JSON.stringify({ type: "object", properties: {} }));Базовая форма
output_schema — объект с type: "object" и картой properties. Каждое
свойство — обычный JSON Schema фрагмент: type, опционально description,
для вложенных структур — properties (object) или items (array).
{
"type": "object",
"properties": {
"document_number": { "type": "string", "description": "Номер документа, как напечатан на бланке" },
"document_date": { "type": "string", "description": "Дата в формате YYYY-MM-DD" },
"total_amount": { "type": "number", "description": "Итоговая сумма с учетом НДС, число без пробелов и валюты" },
"is_paid": { "type": "boolean" }
}
}Поддерживаемые примитивы: string, number, integer, boolean, а также
составные object и array.
Вложенность: объекты внутри объектов
Любая глубина — задавайте properties рекурсивно:
{
"type": "object",
"properties": {
"supplier": {
"type": "object",
"properties": {
"name": { "type": "string" },
"inn": { "type": "string", "description": "10 или 12 цифр, без пробелов" },
"kpp": { "type": "string", "description": "9 цифр, только у юрлиц (не у ИП)" },
"address": {
"type": "object",
"properties": {
"city": { "type": "string" },
"postal_code": { "type": "string" }
}
}
}
}
}
}Массивы: список строк vs список объектов
Для массива обязательно опишите items — иначе элементы вернутся как есть,
без приведения типов:
{
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": { "type": "string" }
},
"items": {
"type": "array",
"description": "Позиции счета/накладной, одна строка = одна позиция таблицы",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"quantity": { "type": "number" },
"unit": { "type": "string", "description": "ед. измерения: шт, кг, м, компл." },
"amount": { "type": "number", "description": "сумма по строке без НДС" }
}
}
}
}
}Длинные документы (на боевом контуре — примерно от 30 000 знаков) с
массивами обрабатываются частями: API режет текст, обрабатывает части
параллельно и сливает массивы с дедупом — вам не нужно самим склеивать
страницы, просто закладывайте, что items[] может прийти из разных
фрагментов документа. Наружу это не торчит: контракт ответа один и тот же.
description — это подсказка модели, не комментарий для человека
description — единственное место, где можно объяснить модели то, что не
следует напрямую из имени поля и типа: формат, единицы измерения, откуда
брать значение при неоднозначности, что делать при отсутствии данных.
// ❌ Мало пользы — просто повторяет имя поля
{ "gross_weight_kg": { "type": "number", "description": "Вес брутто" } }
// ✅ Снимает конкретную неоднозначность документа
{
"gross_weight_kg": {
"type": "number",
"description": "Вес брутто в килограммах из графы 11 накладной ЦМР. Если единица — тонны, переведи в кг. Больше net_weight_kg."
}
}Хорошее правило: если по документу поле можно перепутать с соседним
(нетто/брутто, отправитель/перевозчик, продавец/покупатель — см. коды
weight_invariant_violation, sender_equals_carrier, duplicate_party в
«Ошибках») — опишите словами, как их различить, прямо в
description. Это работает надежнее, чем пытаться закодировать правило в
prompt одной фразой на весь документ.
title — заголовок колонки в Excel
description объясняет поле модели, а title — человеку: при выгрузке
результата файлом (format=xlsx, см. «Перевод и
Excel») заголовки столбцов берутся именно из
title свойств схемы. Без него в шапке окажется технический ключ
(gross_weight_kg вместо «Брутто, кг»).
{
"type": "object",
"properties": {
"shipper": { "type": "string", "title": "Отправитель" },
"date": { "type": "string", "format": "date", "title": "Дата" },
"items": {
"type": "array",
"title": "Позиции",
"items": {
"type": "object",
"properties": {
"name": { "type": "string", "title": "Наименование" },
"quantity": { "type": "number", "title": "Количество" }
}
}
}
}
}format: "date" у строкового поля — привычная пометка «дата в формате
YYYY-MM-DD»; как и остальные ключевые слова JSON Schema, это подсказка
модели, а не серверная валидация (см. ниже).
Чего схема не гарантирует
requiredне отклоняет ответ. Пропущенное значение обычно возвращается какnull, а не как ошибка валидации — трактуйте любое поле как потенциальноnull, даже если пометили его обязательным. На боевом провайдере (Yandex) API технически требует от модели заполнить все поля схемы (structured output это форсирует) — но «заполнить» для не найденного в документе значения означает именноnull.pattern/format/enum/minimum— не строгий валидатор. Это тоже подсказки модели, а не Ajv-проверка на сервере. Критичные инварианты (контрольная сумма ИНН, диапазон дат,total_amount == Σ items[].amount) проверяются на сервере отдельно и приходят какwarnings[]— см. «Ошибки» — но это фиксированный список кодов, не производный от вашей схемы. Свои бизнес-инварианты по-прежнему стоит проверять на своей стороне.- Без
output_schemaрезультата не будет вовсе. Схема — это и есть запрос на данные: без нее модель не вызывается,resultприходитnull, аextraction_status—parse_only. Такой вызов имеет смысл только вместе сinclude(нужен разбор, а не данные). Свободного текстового ответа в контракте нет:result— всегда JSON-объект.
Типичные ошибки новичков — чек-лист
- Схема-строка vs схема-объект перепутаны между multipart-запросом (строка) и JSON-телом (объект) — см. пример выше.
FormData.appendбезJSON.stringifyна JS-стороне — схема тихо игнорируется, а не падает с понятной ошибкой.- Массив объектов без
items— элементы не приводятся к нужным типам, числа могут прийти строками. descriptionдублирует имя поля вместо того, чтобы снимать конкретную неоднозначность документа — самый частый способ получить стабильно посредственный результат на нестандартных документах.- Слишком широкая схема за один вызов — десятки вложенных полей на длинном документе дают менее предсказуемое время ответа и больше пустых полей. Разбивайте на несколько прицельных вызовов вместо одной гигантской схемы «на все».
- Ожидание, что
required/enumжестко провалят запрос — не провалят (см. выше); критичные проверки дублируйте на своей стороне. - Имена полей на русском вместе с кириллическими ключами —
технически работает, но затрудняет код на вашей стороне и типизацию;
держите ключи схемы в
snake_caseна английском, а человеческий контекст — вdescription.
Полный пример
{
"type": "object",
"properties": {
"document_number": { "type": "string" },
"document_date": { "type": "string", "description": "YYYY-MM-DD" },
"supplier": {
"type": "object",
"properties": {
"name": { "type": "string" },
"inn": { "type": "string", "description": "10 или 12 цифр" }
}
},
"customer": {
"type": "object",
"properties": {
"name": { "type": "string" },
"inn": { "type": "string", "description": "10 или 12 цифр" }
}
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"quantity": { "type": "number" },
"amount": { "type": "number", "description": "сумма по строке без НДС" }
}
}
},
"total_amount": { "type": "number", "description": "итоговая сумма с НДС" }
}
}Ответ (см. полный контракт в «/v1/process»):
{
"result": {
"document_number": "0148",
"document_date": "2026-03-12",
"supplier": { "name": "ООО «Балтмонтаж»", "inn": "7801234565" },
"customer": { "name": "ООО «Невский Провиант»", "inn": "7810998874" },
"items": [{ "name": "Термоусадочная пленка", "quantity": 40, "amount": 128000.0 }],
"total_amount": 153600.0
},
"usage": { "pages": 1, "sheets": 1 },
"metadata": { "extraction_status": "full", "cached": false, "…": "…" },
"warnings": []
}