Перейти к содержимому
JSON

Структурированный JSON-вывод Claude в 2026: tool_use, response_format и Pydantic

У Claude нет нативного json_mode — но форсированный tool_use даёт 100% валидный JSON. Прод-паттерн со схемами Pydantic и Zod.

Проблема: LLM галлюцинируют JSON

Любой, кто выводил LLM-фичу в прод, знает эту боль. Просишь модель «вернуть JSON с полями name, email, score» — и в 99% случаев получаешь именно это. В оставшийся 1% получаешь:

  • Открывающий ```json фенс, на котором парсер ломается.
  • Висячий комментарий вида // note: score estimated.
  • Запятую в конце, на которой падает JSON.parse.
  • Невалидную экранировку кавычки внутри строки.
  • Поле, переименованное в e_mail, потому что модели «так красивее».
  • Половину ответа в markdown с пояснением, что значит этот JSON.

На миллионе вызовов в месяц этот 1% — десять тысяч молчаливых прод-фейлов. Свободная генерация текста просто плохо подходит для машинных контрактов.

OpenAI выкатили response_format: { type: "json_schema", strict: true } в 2024 и фактически закрыли тему на своём стеке — декодирование модели ограничивается на уровне токенов, и вывод гарантированно валиден против схемы по грамматике. Anthropic пошли другим путём. По состоянию на 2026 год в нативном Messages API по-прежнему нет флага json_mode. Вместо этого Anthropic предлагают опереться на фичу, которая у них уже была: tool use.

В этой статье — продакшен-паттерн, который мы рекомендуем для получения 100% валидного и соответствующего схеме JSON от Claude. Покрываем нативный API через форсированный tool_use, OpenAI-совместимую обёртку response_format, генерацию схем из Pydantic и Zod, цикл валидация+ретрай и сравнение с нативным json_schema-режимом GPT‑4o.

Все примеры идут на гейтвей Claudexia с базовым URL https://api.claudexia.tech/v1 — он отдаёт и нативный Anthropic Messages API, и OpenAI-совместимый Chat Completions. Цены и список моделей — в нашем посте Цены Claude API 2026.

Ответ Anthropic: форсированный tool_use

Tool use в Claude задумывался для агентных сценариев — чтобы модель могла вызвать get_weather или search_db. Но механизм, которым она эмитит tool-вызовы, ровно то, что нам нужно для структурированного вывода: модель отдаёт блок tool_use, у которого поле input всегда — валидный JSON-объект, соответствующий input_schema инструмента.

Декодер Anthropic это обеспечивает. Модель не «вежливо просят» вернуть JSON; tool input и есть JSON. Добавьте к этому tool_choice: { type: "tool", name: "..." }, чтобы форсировать вызов ровно одного инструмента — и получите структурированный API во всём, кроме названия.

Рецепт:

  1. Опишите целевую форму как Pydantic-модель (Python) или Zod-схему (TypeScript).
  2. Сконвертируйте её в JSON Schema.
  3. Заверните в один инструмент с input_schema = <ваша JSON Schema>.
  4. Отправьте запрос с tools=[этот_инструмент] и tool_choice={"type":"tool","name":"<имя инструмента>"}.
  5. Прочитайте response.content[0].input — это и есть ваш валидированный объект.

Python: Pydantic → JSON Schema → tool_use

import anthropic
from pydantic import BaseModel, Field
from typing import Literal

class InvoiceLineItem(BaseModel):
    description: str
    quantity: int = Field(ge=1)
    unit_price_cents: int = Field(ge=0)

class Invoice(BaseModel):
    invoice_number: str
    issued_on: str = Field(description="ISO-8601 дата, например 2026-04-03")
    currency: Literal["USD", "EUR", "RUB", "GBP"]
    vendor_name: str
    line_items: list[InvoiceLineItem]
    total_cents: int

client = anthropic.Anthropic(
    base_url="https://api.claudexia.tech/v1",
    api_key="sk-cxa-...",
)

extract_tool = {
    "name": "record_invoice",
    "description": "Записать распарсенный инвойс в учётную систему.",
    "input_schema": Invoice.model_json_schema(),
}

resp = client.messages.create(
    model="claude-sonnet-4.5",
    max_tokens=2048,
    tools=[extract_tool],
    tool_choice={"type": "tool", "name": "record_invoice"},
    messages=[
        {"role": "user", "content": f"Извлеки инвойс:\n\n{raw_invoice_text}"}
    ],
)

raw = resp.content[0].input          # уже dict, уже валиден против схемы
invoice = Invoice.model_validate(raw)  # дополнительная Pydantic-проверка

Значение resp.content[0].input приходит как Python-словарь, который уже парсится чисто и уже удовлетворяет JSON Schema, которую вы прислали. Дополнительный Invoice.model_validate(raw) — на всякий случай: он даёт Pydantic-приведение типов (например, строка "42" → int 42) и запускает ваши кастомные валидаторы.

TypeScript: Zod → JSON Schema → tool_use

import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

const SupportTicket = z.object({
  category: z.enum(["billing", "bug", "feature_request", "abuse", "other"]),
  urgency: z.enum(["low", "normal", "high", "critical"]),
  summary: z.string().max(280),
  suggested_owner_team: z.enum(["payments", "platform", "growth", "trust_safety"]),
  contains_pii: z.boolean(),
});

const client = new Anthropic({
  baseURL: "https://api.claudexia.tech/v1",
  apiKey: process.env.CLAUDEXIA_KEY!,
});

const resp = await client.messages.create({
  model: "claude-sonnet-4.5",
  max_tokens: 1024,
  tools: [{
    name: "classify_ticket",
    description: "Классифицировать входящий тикет поддержки.",
    input_schema: zodToJsonSchema(SupportTicket, { target: "openAi" }) as any,
  }],
  tool_choice: { type: "tool", name: "classify_ticket" },
  messages: [{ role: "user", content: ticketBody }],
});

const block = resp.content.find((b) => b.type === "tool_use");
const ticket = SupportTicket.parse(block?.input);

Обратите внимание на target: "openAi" в zodToJsonSchema — он выдаёт вариант JSON Schema, который Claude (и OpenAI) принимают без претензий. Дефолтный таргет Zod эмитит $ref-паттерны, на которых Anthropic API ругается.

OpenAI-совместимый путь: response_format на гейтвее Claudexia

Если у вас уже есть кодовая база на OpenAI SDK, гейтвей Claudexia принимает форму OpenAI Chat Completions и под капотом транслирует response_format в форсированный tool_use. Переписывать ничего не надо.

from openai import OpenAI

client = OpenAI(
    base_url="https://api.claudexia.tech/v1",
    api_key="sk-cxa-...",
)

resp = client.chat.completions.create(
    model="claude-sonnet-4.5",
    messages=[{"role": "user", "content": resume_text}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "Resume",
            "strict": True,
            "schema": Resume.model_json_schema(),
        },
    },
)

resume = Resume.model_validate_json(resp.choices[0].message.content)

Это путь наименьшего сопротивления при миграции с OpenAI. Тот же SDK, та же семантика response_format, качество Claude.

Три конкретных примера

1. Извлечение инвойса

Вход: шумный текст инвойса, выдранный из PDF. Выход: модель Invoice из Python-сниппета выше. Форсированный tool_use означает, что вам больше никогда не нужно писать regex для парсинга позиций — модель возвращает типизированный список, и downstream-код напрямую читает invoice.line_items[0].unit_price_cents.

2. Классификация тикета поддержки

Используем Zod-схему SupportTicket. Энумы category и urgency ограничивают модель так, что роутить результат можно без защитных .toLowerCase() повсюду. contains_pii — булев флаг, который сразу прокидывается в пайплайн редактирования персональных данных.

3. Парсинг резюме

class WorkExperience(BaseModel):
    company: str
    title: str
    started_on: str = Field(description="YYYY-MM")
    ended_on: str | None = Field(description="YYYY-MM или null, если текущая работа")
    highlights: list[str]

class Resume(BaseModel):
    full_name: str
    email: str | None
    phone: str | None
    years_of_experience: int = Field(ge=0, le=60)
    skills: list[str]
    experience: list[WorkExperience]

Подключаем ту же обвязку с tool_use — и получаем API парсинга резюме.

Советы по дизайну схем

После пары десятков таких пайплайнов в проде, важнее всего оказывается несколько паттернов.

  • Используйте enum агрессивно. Любое поле, чьи значения принадлежат конечному множеству, должно быть энумом. Модель сильно точнее выбирает из списка, чем генерит свободно.
  • Descriptions — не украшение. Claude читает description каждого поля. «ISO-8601 дата» или «телефон в формате E.164» меняют вывод. Относитесь к ним как к промтам.
  • Держите вложенность мелкой. Два уровня — оптимум. На трёх-четырёх модель начинает терять, в каком { находится, даже с форсированным tool use.
  • Строковые энумы лучше булевых для трёх состояний. status: "approved" | "rejected" | "needs_review" рассуждается лучше, чем два независимых булевых флага.
  • Явно помечайте опциональные поля. Optional[str] в Pydantic / .optional() в Zod. Модель аккуратно их пропустит, а не придумает "unknown".
  • Не просите свободный текст внутри структурных полей. Поле summary: str в конце — нормально. summary, за которым идут ещё структурные поля, обычно проливает прозу в эти соседние поля.

Цикл валидация и ретрай

Даже с форсированным tool_use ваши бизнес-валидаторы (например, «total_cents должен равняться сумме позиций») всё равно могут падать. Паттерн прямой:

def call_with_retry(messages, max_attempts=3):
    for attempt in range(max_attempts):
        resp = client.messages.create(
            model="claude-sonnet-4.5",
            max_tokens=2048,
            tools=[extract_tool],
            tool_choice={"type": "tool", "name": "record_invoice"},
            messages=messages,
        )
        raw = resp.content[0].input
        try:
            return Invoice.model_validate(raw)
        except ValidationError as e:
            messages.append({"role": "assistant", "content": resp.content})
            messages.append({
                "role": "user",
                "content": f"Этот вывод не прошёл валидацию: {e}. Вызови инструмент ещё раз с исправленными значениями.",
            })
    raise RuntimeError("Валидация схемы не прошла после ретраев")

Одного ретрая обычно хватает, и на второй попытке Claude видит исходный вывод и ошибку валидации в контексте — модель надёжно сама себя поправляет.

GPT‑4o json_schema vs форсированный tool_use Claude

Оба подхода в проде дают JSON, соответствующий схеме. Практические отличия:

  • Поверхность гарантии. OpenAI-овский strict: true обеспечивается ограниченным декодированием на уровне токенов — модель буквально не может выдать невалидный JSON. Anthropic-овский форсированный tool_use обеспечивается валидатором на tool input. На API-поверхности разницы нет (оба возвращают валидный JSON всегда), но если Claude всё-таки промахнётся, режим отказа — «вызов вернул ошибку», а не «JSON битый».
  • Поддержка фич схемы. Strict-режим OpenAI запрещает oneOf, not, рекурсивные ссылки и ещё пару фич JSON Schema. Claude в tool_use принимает более широкий поднабор, включая некоторые $ref-паттерны (если их сгенерил правильный таргет Zod) и более богатые pattern-строки.
  • Использование descriptions. Claude взвешивает описания полей сильнее, чем GPT‑4o. Если у схемы богатые description, ждите, что Claude из них выжмет больше.
  • Latency. Форсированный tool_use добавляет небольшой фиксированный оверхед против свободной генерации, но на наших бенчмарках сопоставим с json_schema-режимом GPT‑4o.
  • Стоимость миграции. Через OpenAI-совместимый эндпоинт гейтвея Claudexia вы меняете одну строчку — параметр model — и response_format продолжает работать.

Итого

Claude не нужен флаг json_mode, потому что tool use уже даёт нечто более сильное: типизированный контракт, который форсит API. Опишите схему один раз в Pydantic или Zod, форсируйте один инструмент — и у вас структурированный вывод, не уступающий ничему на рынке. OpenAI-совместимая обёртка на гейтвее Claudexia позволяет уронить Claude в существующую response_format-кодовую базу без изменений в бизнес-логике. Добавьте цикл ретрая на свои бизнес-валидаторы — и получится JSON-пайплайн, который просто не ломается.