APIDOCINSPECT

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_statusparse_only. Такой вызов имеет смысл только вместе с include (нужен разбор, а не данные). Свободного текстового ответа в контракте нет: result — всегда JSON-объект.

Типичные ошибки новичков — чек-лист

  1. Схема-строка vs схема-объект перепутаны между multipart-запросом (строка) и JSON-телом (объект) — см. пример выше.
  2. FormData.append без JSON.stringify на JS-стороне — схема тихо игнорируется, а не падает с понятной ошибкой.
  3. Массив объектов без items — элементы не приводятся к нужным типам, числа могут прийти строками.
  4. description дублирует имя поля вместо того, чтобы снимать конкретную неоднозначность документа — самый частый способ получить стабильно посредственный результат на нестандартных документах.
  5. Слишком широкая схема за один вызов — десятки вложенных полей на длинном документе дают менее предсказуемое время ответа и больше пустых полей. Разбивайте на несколько прицельных вызовов вместо одной гигантской схемы «на все».
  6. Ожидание, что required/enum жестко провалят запрос — не провалят (см. выше); критичные проверки дублируйте на своей стороне.
  7. Имена полей на русском вместе с кириллическими ключами — технически работает, но затрудняет код на вашей стороне и типизацию; держите ключи схемы в 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": []
}

On this page