Перейти к основному содержанию

Claude Structured Outputs: надёжный JSON по схеме и ошибки 400

Схему кладут в output_config.format, JSON приходит в текстовом блоке. minimum и рекурсия дают 400, а minLength шлюз принял, но не соблюдал.

LaoZhang AI TeamОпубликовано11 мин чтения
Содержание
Claude Structured Outputs: схема в output_config.format даёт JSON, minimum и рекурсивный $ref дают 400, minLength принят, но не соблюдается

У Claude есть structured outputs, и по состоянию на 30 октября 2026 года это общедоступная функция Claude API, beta-заголовок для неё больше не нужен. Чтобы получить JSON, который код разберёт без правок, передайте JSON Schema в поле запроса output_config.format с "type": "json_schema". Готовый JSON придёт в текстовом блоке ответа: обычно это content[0].text, а если включён thinking, то первый блок с "type": "text". Если по схеме должны приходить аргументы вызова инструмента, а не сам ответ, ставьте strict: true в описании инструмента; обе функции можно включить в одном запросе.

Гарантия касается синтаксиса, обязательных полей и типов, но не всего остального. Перед разбором проверяйте stop_reason: при refusal и max_tokens JSON может не совпасть со схемой или оборваться на середине. Ограничения вроде minimum и minLength декодер не соблюдает, поэтому проверять их придётся в своём коде, а SDK делает это за вас только в хелперах messages.parse(). Старый верхнеуровневый параметр output_format из гайдов бета-периода устарел: по документации Anthropic без beta-заголовка он даёт 400.

Через прокси та же ошибка бывает тихой. 30 октября 2026 года запрос со старым output_format через LaoZhang API (Anthropic-совместимый /v1/messages) вернул 200 и обычный текст вместо JSON, а число входных токенов осталось таким же, как без схемы. Поэтому на любом шлюзе сначала убедитесь, что пришёл именно JSON и что usage.input_tokens вырос по сравнению с запросом без схемы. Прямой api.anthropic.com в этих тестах не участвовал, и результаты «через шлюз» ниже относятся только к этому маршруту.

Примечание: laozhang.ai — наш собственный сервис; его цены — наши тарифы, а не независимое сравнение.

Запрос с output_config.format: cURL и где лежит JSON

Минимальный рабочий запрос — обычный вызов Messages API с одним дополнительным полем output_config.format. Внутри лежит JSON Schema, и у каждого объекта в ней должно стоять "additionalProperties": false. Пример ниже взят из документации Anthropic: письмо клиента превращается в четыре поля.

bash
curl https://api.anthropic.com/v1/messages \
  -H "content-type: application/json" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-opus-5-5",
    "max_tokens": 1024,
    "messages": [{
      "role": "user",
      "content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm."
    }],
    "output_config": {
      "format": {
        "type": "json_schema",
        "schema": {
          "type": "object",
          "properties": {
            "name": {"type": "string"},
            "email": {"type": "string"},
            "plan_interest": {"type": "string"},
            "demo_requested": {"type": "boolean"}
          },
          "required": ["name", "email", "plan_interest", "demo_requested"],
          "additionalProperties": false
        }
      }
    }
  }'

Ответ — обычное сообщение, но в content[0].text лежит строка с JSON, которую можно сразу передать в json.loads() или JSON.parse(). 30 октября 2026 года тот же запрос с моделью claude-haiku-5-5 через LaoZhang API (адрес https://api.laozhang.ai/v1/messages, остальное без изменений) вернул за 3,61 с stop_reason: "end_turn" и текст {"demo_requested": true, "email": "john@example.com", "name": "John Smith", "plan_interest": "Enterprise"}. Входных токенов было 333. Тот же промпт без схемы дал markdown-список вместо JSON и 64 входных токена: разница в 269 токенов — это схема и служебный промпт, который Claude получает вместе с ней.

Примечание: laozhang.ai — наш собственный сервис; его цены — наши тарифы, а не независимое сравнение.

Функция работает на моделях от поколения 4.5 и новее: Opus 4.5–5.5, Sonnet 4.5–5.5, Haiku 4.5 и 5.5, Fable 5 и 5.1, Mythos. Claude 3.x и Opus 4.1 в списке поддерживаемых моделей в документации Anthropic по structured outputs нет.

Python и Pydantic: messages.parse() и parsed_output

В Python удобнее всего описать ответ моделью Pydantic и вызвать client.messages.parse(). Хелпер сам превращает модель в схему, отправляет её в output_config.format, проверяет ответ и возвращает готовый объект в response.parsed_output. Аргумент здесь называется output_format, но это имя параметра хелпера, а не устаревшее поле запроса: в API SDK отправит именно output_config.format.

python
from typing import Literal, Optional
from anthropic import Anthropic
from pydantic import BaseModel, Field

class Lead(BaseModel):
    name: str = Field(min_length=2)
    email: str
    plan: Literal["Starter", "Team", "Enterprise"]
    seats: int = Field(ge=1, le=500)
    phone: Optional[str] = None

client = Anthropic()  # для шлюза: Anthropic(base_url="https://api.laozhang.ai", api_key="...")

response = client.messages.parse(
    model="claude-haiku-5-5",
    max_tokens=512,
    messages=[{"role": "user", "content": "John Smith (john@example.com) wants the Enterprise plan for 40 seats."}],
    output_format=Lead,
)
print(response.parsed_output)

В модели есть min_length, ge и le, которых API не принимает, и SDK обходит это преобразованием схемы. Его результат можно посмотреть без запроса через transform_schema(Lead). В anthropic 2.3.0 с pydantic 2.14 поле name ушло в API как обычная строка с описанием {minLength: 2}, seats — как integer с описанием {maximum: 500, minimum: 1}, необязательный phone превратился в anyOf из строки и null, а ко всему объекту добавилось additionalProperties: false. Модель видит ограничения только как подсказку в описании, а соблюдение проверяет уже Pydantic после ответа.

Это видно на проверке 30 октября 2026 года через LaoZhang API. Обычный запрос вернул parsed_output с seats=40 и phone=None при 489 входных токенах. Во втором запросе в тексте честно было указано «0 seats»: API вернул "seats": 0, и SDK выбросил pydantic.ValidationError с сообщением Input should be greater than or equal to 1. Значит, messages.parse() нужно оборачивать в обработку ValidationError: схема не мешает модели передать значение, нарушающее ваше правило.

Примечание: laozhang.ai — наш собственный сервис; его цены — наши тарифы, а не независимое сравнение.

В TypeScript то же самое делают через Zod: схема оборачивается в zodOutputFormat() и передаётся в output_config.format вызова client.messages.parse(), результат приходит в parsed_output.

typescript
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";

const Contact = z.object({ name: z.string(), email: z.string(), plan_interest: z.string(), demo_requested: z.boolean() });

const response = await new Anthropic().messages.parse({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Extract the key information from this email: ..." }],
  output_config: { format: zodOutputFormat(Contact) },
});
console.log(response.parsed_output);

Если вместо Zod используете jsonSchemaOutputFormat(), учтите, что он только преобразует схему, а ответ не проверяет. Java SDK поступает наоборот: неподдерживаемые ограничения он не переносит в описание, а отклоняет схему ещё на вашей стороне, до отправки запроса.

JSON outputs или strict: true в инструменте: что выбрать

Выбор зависит от того, что должно соответствовать схеме. Если нужен сам ответ модели в виде данных (извлечение полей, классификация, итоговый отчёт агента), это JSON outputs через output_config.format. Если модель вызывает вашу функцию и её аргументы должны точно совпасть с типами, это strict: true в определении инструмента. В агенте, который и вызывает инструменты, и отдаёт итог в виде JSON, включайте оба: так строит пример и документация Anthropic.

JSON outputsStrict tool use
Где задаётсяoutput_config.format в запросе"strict": true рядом с name и input_schema инструмента
Что ограничиваетТекст ответа ClaudeИмя инструмента и его input
Где результатТекстовый блок, stop_reason: "end_turn"Блок tool_use, stop_reason: "tool_use"
Типичная задачаИзвлечь данные, классифицировать, вернуть отчётВызвать функцию с правильными типами (2, а не "two")
Ограничения схемыОбщие: тот же поднабор JSON SchemaТе же, плюс не больше 20 strict-инструментов в запросе

Без strict-режима модель может передать в целочисленный параметр строку "2" или "two" или пропустить обязательное поле; со strict: true аргументы проходят через ту же грамматику, что и JSON outputs. 30 октября 2026 года через LaoZhang API инструмент create_demo_booking с перечислениями для тарифа и дня недели, целым hour_24 и форматом email при принудительном tool_choice вернул stop_reason: "tool_use" и input {"email": "john@example.com", "hour_24": 14, "plan": "Enterprise", "weekday": "Tue"}. Фраза «next Tuesday at 2pm» превратилась в "Tue" и 14, то есть в значения из схемы, а не в свободный текст.

Примечание: laozhang.ai — наш собственный сервис; его цены — наши тарифы, а не независимое сравнение.

json
{
  "name": "create_demo_booking",
  "description": "Book a product demo for a lead.",
  "strict": true,
  "input_schema": {
    "type": "object",
    "properties": {
      "email": {"type": "string", "format": "email"},
      "plan": {"type": "string", "enum": ["Starter", "Team", "Enterprise"]},
      "weekday": {"type": "string", "enum": ["Mon", "Tue", "Wed", "Thu", "Fri"]},
      "hour_24": {"type": "integer"}
    },
    "required": ["email", "plan", "weekday", "hour_24"],
    "additionalProperties": false
  }
}

Помечать строгими все инструменты подряд не стоит. Лимиты сложности считаются по всем strict-схемам запроса вместе, поэтому strict: true разумно оставлять там, где неверный тип действительно ломает ваш код. Подробности — в документации Anthropic по strict tool use.

Какие ключевые слова JSON Schema дают 400, а какие не соблюдаются

Structured outputs понимают только часть JSON Schema, и за её пределами возможны три исхода: ошибка 400, молчаливое принятие без соблюдения и нормальная работа. Документация Anthropic обещает 400 для любого неподдерживаемого ключевого слова, но через шлюз 30 октября 2026 года это подтвердилось не для всех. Ниже — что сказано в документации и что вернул LaoZhang API на claude-haiku-5-5; тексты ошибок приведены без перевода, как их вернул шлюз.

Примечание: laozhang.ai — наш собственный сервис; его цены — наши тарифы, а не независимое сравнение.

Ключевое словоПо документации AnthropicЧерез LaoZhang API, 30.10.2026Что делать
minimum, maximum, multipleOfНе поддерживаются, 400minimum: 1 → 400: output_config.format.schema: For 'integer' type, property 'minimum' is not supportedУбрать из схемы (SDK делает это сам), проверять диапазон в коде
minLength, maxLengthНе поддерживаются, 400Приняты (200); при minLength: 12 и maxLength: 12 на просьбу вернуть код ABC пришло {"code": "ABC"}Проверять длину в коде: ограничение не работает, хотя ошибки нет
Рекурсивный $refНе поддерживается400: Invalid schema: Circular reference detected in schema definitions: node -> node. Self-referencing or mutually-referencing definitions are not supported.Развернуть дерево до фиксированной глубины
additionalPropertiesОбязательно false у каждого объектаБез него 200, ответ совпал со схемойВсё равно ставить false явно: на другом маршруте может быть 400
enumТолько строки, числа, булевы и null; регистр не гарантирован5 запросов из 5 вернули "Conversation topic 3" точно как в схемеСравнивать значения без учёта регистра
patternПростые regex; без lookahead/lookbehind, \b, обратных ссылок и больших {n,m}—Сложные проверки переносить в код
minItemsТолько 0 или 1—Проверять число элементов в коде

Остальное поддерживается: все базовые типы, const, anyOf, allOf (кроме allOf с $ref), локальные $ref и $defs, default, а из форматов строк — date-time, time, date, duration, email, hostname, uri, ipv4, ipv6 и uuid. Вложенные объекты тоже допустимы, так что «только плоские схемы» — это не ограничение API; запрещена рекурсия, а не вложенность.

Отдельно действуют лимиты сложности на весь запрос: не больше 20 strict-инструментов, 24 необязательных параметров и 16 параметров с объединением типов (anyOf или "type": ["string", "null"]), считая все строгие схемы вместе. Даже если они соблюдены, слишком большая грамматика даёт 400 Schema is too complex for compilation, а компиляция прерывается через 180 секунд. Anthropic советует по порядку: оставить strict только для важных инструментов, сделать параметры обязательными, упростить вложенность и разбить запрос на несколько.

Вместо JSON пришёл обычный текст: схема не дошла до модели

Если ответ пришёл обычным текстом, почти всегда схема не дошла до модели, и то, как это проявится, зависит от маршрута. Прямой API Anthropic по документации отвечает 400 на устаревший output_format без заголовка anthropic-beta: structured-outputs-2025-11-13. Через LaoZhang API 30 октября 2026 года тот же запрос и без заголовка, и с ним вернул 200 и markdown-список, причём входных токенов было 64, ровно как у запроса без схемы. Шлюз отбросил неизвестное поле, ничего не сообщив.

Примечание: laozhang.ai — наш собственный сервис; его цены — наши тарифы, а не независимое сравнение.

Три запроса через LaoZhang API: со схемой в output_config.format пришёл JSON и 333 входных токена, без схемы и со старым output_format — markdown-список и 64 токена

Обратная ситуация — громкая ошибка там, где запрос правильный. Каналы, которые проверяют тело запроса по правилам Bedrock, отвечают 400 ValidationException с текстом Extra inputs are not permitted на поля, которых они не принимают; встречается, например, output_config effort extra inputs are not permitted. Об этом прямо пишет справка LaoZhang API об ошибке 400 ValidationException, а разбор других текстов 400 есть в статье «Claude в AWS Bedrock отвечает 400: причина по тексту ошибки».

Когда что-то из этого случилось, пройдите по порядку:

  • Убедитесь, что схема лежит в output_config.format, а не в верхнеуровневом output_format из старых примеров и не в response_format: это имя поля из API OpenAI, в Messages API Anthropic его нет.
  • Сравните usage.input_tokens с тем же запросом без схемы. Если число не изменилось, схема до модели не дошла, даже если статус 200.
  • Посмотрите, начинается ли content[0].text с { и проходит ли json.loads(). Markdown или текст с пояснениями означает, что формат не применился.
  • Если запрос идёт через обёртку (LiteLLM, фреймворк агентов, собственный прокси), проверьте в логах исходящий запрос: передаёт ли она output_config дальше без изменений.
  • При 400 Extra inputs are not permitted уберите из запроса поля, которые канал не принимает, или переключитесь на маршрут, который передаёт output_config полностью.

Где «всегда валидный JSON» не гарантирован: отказ, max_tokens, ваша проверка

Грамматика гарантирует форму ответа, только если генерация закончилась нормально, то есть с stop_reason: "end_turn" для JSON outputs или "tool_use" для strict-инструментов. Во всех остальных случаях JSON может не совпасть со схемой, а ограничения, которые API не поддерживает, вы проверяете сами. Код, который сразу делает json.loads() и кладёт результат в базу, рано или поздно на этом сломается.

  • Проверяйте stop_reason до разбора. При refusal Claude отказался отвечать по соображениям безопасности: статус 200, токены списаны, а текст отказа важнее схемы. Не делайте поле вида «пошаговое рассуждение» — оно может вызвать отказ reasoning_extraction; просите короткое объяснение.
  • Ловите max_tokens. 30 октября 2026 года через LaoZhang API запрос 15 идей при max_tokens: 30 вернул 200 и текст {"ideas": ["REST vs GraphQL vs gRPC: How to Choose the, который не разбирается. Повторяйте с бо́льшим лимитом, а не пытайтесь «дочинить» строку.
  • Валидируйте значения после ответа: длины, диапазоны, регулярные выражения, число элементов массива. Пример с "seats": 0 в разделе про Python показывает, что модель вернёт значение, нарушающее ваше правило, если так написано во входных данных.
  • Не полагайтесь на порядок ключей. Документация обещает порядок схемы с обязательными полями впереди, но во всех ответах через LaoZhang API 30 октября 2026 года ключи шли по алфавиту. Обращайтесь к полям по имени.
  • Считайте необязательное поле «отсутствующим или null». В том же тесте поле phone с типом string не было ни обязательным, ни nullable, но пришло как "phone": null вместо того, чтобы пропасть из ответа.
  • Сравнивайте enum без учёта регистра и не заводите значения, которые отличаются только регистром. Anthropic предупреждает, что вместо "Conversation topic 3" может прийти "Conversation Topic 3" без всякой ошибки.

Примечание: laozhang.ai — наш собственный сервис; его цены — наши тарифы, а не независимое сравнение.

Ещё две несовместимости дают 400 сразу. Citations вместе с output_config.format API не принимает. Предзаполнение ответа { в последнем сообщении assistant тоже не работает: на claude-haiku-5-5 через шлюз пришло This model does not support assistant message prefill. The conversation must end with a user message. Как заменить prefill на новых моделях, разобрано в статье «Ошибка 400 prefill в Claude: на каких моделях и чем заменить».

Три проверки ответа перед кодом: stop_reason, начало текста с фигурной скобки и валидация Pydantic, на которой "seats": 0 даёт ValidationError

Все проверки помещаются в одну функцию-страховку. Она работает с сырым messages.create(), поэтому годится и там, где messages.parse() недоступен:

python
from anthropic import Anthropic, transform_schema
from pydantic import ValidationError

client = Anthropic()
SCHEMA = transform_schema(Lead)  # модель Lead из раздела про Python

class NotJSON(Exception):
    pass

def extract_lead(text: str) -> Lead | None:
    resp = client.messages.create(
        model="claude-haiku-5-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": text}],
        output_config={"format": {"type": "json_schema", "schema": SCHEMA}},
    )
    if resp.stop_reason == "refusal":
        return None  # отказ: записать в лог, не разбирать
    if resp.stop_reason == "max_tokens":
        raise NotJSON("ответ обрезан: увеличьте max_tokens и повторите")
    raw = next(b.text for b in resp.content if b.type == "text")
    if not raw.lstrip().startswith("{"):
        raise NotJSON("схема не применилась: проверьте output_config на маршруте")
    try:
        return Lead.model_validate_json(raw)  # длины, диапазоны, значения enum
    except ValidationError as err:
        raise NotJSON(f"JSON не прошёл проверку: {err}") from err

Если в модели есть Literal, расхождение в регистре тоже даст здесь ValidationError. Когда "Conversation Topic 3" и "Conversation topic 3" для вас одно и то же, приводите такие поля к нужному написанию валидатором с mode="before" до основной проверки.

Сколько стоит формат: +269 входных токенов на запрос

Structured outputs не тарифицируются отдельно, но Claude получает дополнительный системный промпт с описанием формата, и эти токены оплачиваются как обычный вход. В замере через шлюз из раздела про cURL схема из четырёх полей добавила 269 входных токенов на запрос. Для расчёта ниже эти 269 токенов умножены на прайс Anthropic за входные токены: 269 × 1 млн запросов = 269 млн токенов, умноженные на цену за 1 млн.

МодельЦена входа за 1 млн токеновНадбавка на 1 млн запросов
Claude Haiku 5.5 (промпт до 100 тыс. токенов)$0,10$26,90
Claude Sonnet 5.5$2$538
Claude Opus 5.5$4$1 076

Цены взяты со страницы цен Anthropic по состоянию на 30 октября 2026 года; в Batch API они вдвое ниже. Расчёт верен для одной схемы и одного промпта: схема побольше добавит больше токенов. Выход в том же примере, наоборот, сократился с 91 токена прозы до 46 токенов JSON. На Sonnet 5.5 это экономит около $0,00045 на вызов при надбавке на входе около $0,000538, так что итог зависит от того, насколько многословен ваш ответ без схемы. Сравнение цен Claude с другими API — в статье «Claude API или OpenAI API: где дешевле при вашей нагрузке».

Две вещи влияют на счёт косвенно. Смена output_config.format сбрасывает кэш промпта для этой переписки, так что схему лучше не менять от запроса к запросу. Первый запрос с новой схемой, по документации, идёт медленнее из-за компиляции грамматики, которая потом кэшируется на 24 часа с последнего использования. Через шлюз эта задержка не выделилась: новая схема и два повтора ответили за 4,23, 3,33 и 4,28 с, и разброс сети оказался больше.

Частые вопросы о Claude structured outputs

Работают ли structured outputs в Amazon Bedrock и Google Cloud?

Да. По документации Anthropic на 30 октября 2026 года функция общедоступна в Claude API, Claude Platform on AWS, Amazon Bedrock (включая InvokeModel), Claude на Google Cloud (Vertex AI) и Microsoft Foundry. Недоступна она только в Managed Agents. Если облако или шлюз отвечает 400 на output_config, смотрите раздел про текст вместо JSON выше.

Можно ли включить structured outputs со streaming и Batch API?

Да, по документации Anthropic оба режима совместимы: ответ стримится как обычно, а в Batch API запросы со схемой стоят вдвое дешевле. Подсчёт токенов через token counting тоже работает и компиляцию схемы не запускает. Учтите, что при стриминге JSON становится валидным только в конце, поэтому разбирайте его после завершения потока.

Совместимы ли structured outputs с extended thinking?

Да. Грамматика ограничивает только итоговый ответ, а не блоки thinking, вызовы инструментов и их результаты. 30 октября 2026 года через LaoZhang API claude-sonnet-5-5 с thinking: {"type": "adaptive"} и output_config: {"effort": "low", "format": ...} вернул один текстовый блок с валидным JSON; блока thinking при низком effort не было. Поэтому JSON ищите по типу блока, а не по индексу.

Есть ли у Claude JSON mode, как у OpenAI?

Отдельного JSON mode у Claude нет: его роль выполняет output_config.format со схемой, а режим «просто валидный JSON без схемы» не предусмотрен. Как то же самое устроено у OpenAI, разобрано в статье «Structured Outputs или Function Calling в OpenAI: что выбрать».

Structured outputs в Claude Code и Agent SDK — это то же самое?

Нет, это другой механизм: схема передаётся в query() через outputFormat (TypeScript) или output_format (Python), SDK сам проверяет итог агента, при несовпадении просит переделать и возвращает данные в structured_output. Подробности — в документации Claude Agent SDK по structured outputs.

Можно ли класть персональные данные в JSON-схему при ZDR или HIPAA?

В саму схему — нет. По документации Anthropic промпты и ответы со structured outputs не хранятся при Zero Data Retention, но схема кэшируется до 24 часов с последнего использования и не получает той же защиты. Поэтому имена свойств, значения enum и const и регулярные выражения в pattern не должны содержать PHI, то есть защищённые медицинские данные пациентов; им место только в тексте сообщений. По той же причине не стоит класть в схему и другие персональные данные.

Источники4

Внешние страницы, на которые ссылается это руководство, в порядке упоминания. Последнее обновление: 30 окт. 2026 г..

  1. 1.документации Anthropic по structured outputsplatform.claude.com/docs/en/build-with-claude/structured-outputs
  2. 2.документации Anthropic по strict tool useplatform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use
  3. 3.страницы цен Anthropicplatform.claude.com/docs/en/about-claude/pricing
  4. 4.документации Claude Agent SDK по structured outputscode.claude.com/docs/en/agent-sdk/structured-outputs
Два пути получения ответа: готовый объект и запрос инструмента с исполнением в приложении
Руководства по API

Structured Outputs или Function Calling в OpenAI: что выбрать

Structured Outputs задаёт форму ответа, а Function Calling передаёт приложению запрос на работу. Строгие аргументы функции тоже используют Structured Outputs; выбор зависит от того, нужны ли внешние данные и кому предназначен финальный объект.

8 мин
Ошибка неподдерживаемой роли в API и выбор подходящего формата сообщений
Руководства по API

Как исправить API 400 с неподдерживаемой ролью и не сломать системные инструкции

Ошибка unsupported role требует проверки конечного запроса: начальные инструкции, правила в середине диалога и результаты инструментов передаются по-разному. Исправляйте формат для своего API, сохраняя приоритет правил и связи в истории.

10 мин