Claude Structured Outputs — output_config.format と messages.parse で JSON を確実に取得する

Claude API の Structured Outputs が新 API `output_config.format` に統一されました。旧 `output_format` 引数は deprecated、推奨は `client.messages.parse()` + Zod / Pydantic スキーマの自動検証です。JSON Schema の制約、strict tool use、refusal の扱いまで、実装で使えるパターンをまとめます。

Claude API の Structured Outputs が API 正規形を output_config.format に寄せ、TypeScript / Python SDK で client.messages.parse() が推奨パスになりました。旧 output_format 引数は deprecated(まだ動く)。本記事では、新 API の使い方、Zod との組み合わせ、Strict Tool Use、refusal の扱いまでをまとめます。

到達点

  • client.messages.parse() + Zod スキーマで、LLM 出力を型付き・検証済みの JS オブジェクトとして受ける
  • messages.create() を直接呼ぶときも output_config.format に寄せる
  • 関数(tool)呼び出しの入力を strict: true で schema 厳密化
  • stop_reason: "refusal" で schema が守られないケースを安全にハンドリング

対象モデル(2026-04 時点): Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5(legacy としては Opus 4.5 / Opus 4.1 も対応)。

1. 推奨パス — client.messages.parse() + Zod

TypeScript SDK には zodOutputFormat ヘルパが用意されており、Zod スキーマから自動で JSON Schema を生成 → output_config.format に乗せ → レスポンスを検証までを一括で面倒を見ます。

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

const ContactSchema = z.object({
  name: z.string(),
  email: z.string(),
  plan: z.string(),
  interests: z.array(z.string()),
  demo_requested: z.boolean(),
});

const client = new Anthropic();

const res = await client.messages.parse({
  model: "claude-opus-4-7",
  max_tokens: 4_000,
  messages: [
    {
      role: "user",
      content:
        "抽出して: Jane Doe (jane@co.com) wants Enterprise, interested in API & SDKs, wants a demo.",
    },
  ],
  output_config: {
    format: zodOutputFormat(ContactSchema),
  },
});

// parsed_output は null 許容なので assert / guard してから使う
console.log(res.parsed_output!.name);         // "Jane Doe"
console.log(res.parsed_output!.interests);    // string[]
console.log(res.parsed_output!.demo_requested); // true

Python でも同じ構造で、Pydantic モデルを渡すヘルパが用意されています。手書きの JSON Schema を書かずに済むのがこのパスの価値です。

2. 直接 messages.create() を呼ぶ場合

parse() を使わず素の API を叩くなら、output_config.format に JSON Schema を渡します。

const res = await client.messages.create({
  model: "claude-opus-4-7",
  max_tokens: 4_000,
  messages: [{ role: "user", content: "..." }],
  output_config: {
    format: {
      type: "json_schema",
      name: "Contact",
      schema: {
        type: "object",
        properties: {
          name: { type: "string" },
          email: { type: "string" },
          plan: { type: "string" },
          interests: { type: "array", items: { type: "string" } },
          demo_requested: { type: "boolean" },
        },
        required: ["name", "email", "plan", "interests", "demo_requested"],
        additionalProperties: false,
      },
    },
  },
});

output_format パラメータは deprecated です。まだ動きますが、新規コードは output_config.format で統一します。

3. Strict Tool Use — strict: true

ツール呼び出し側でも strict: true で引数スキーマを厳密化できます。

await client.messages.create({
  model: "claude-opus-4-7",
  max_tokens: 4_000,
  messages: [{ role: "user", content: "Book a flight to Tokyo for 2 on 2026-04-25" }],
  tools: [
    {
      name: "book_flight",
      description: "Book a flight to a destination",
      strict: true, // ← schema 厳密化
      input_schema: {
        type: "object",
        properties: {
          destination: { type: "string" },
          date: { type: "string", format: "date" },
          passengers: { type: "integer", enum: [1, 2, 3, 4, 5, 6, 7, 8] },
        },
        required: ["destination", "date", "passengers"],
        additionalProperties: false,
      },
    },
  ],
});

strict ツールでは、スキーマに合致しない引数はそもそも生成されません。自前バリデーションが不要になり、tool 実装側のガードを薄くできます。

4. JSON Schema の制約

Claude の Structured Outputs は JSON Schema の サブセットを解釈します。

サポート: object / array / string / integer / number / boolean / null / enum / const / anyOf / allOf / $ref / $def / 主要な string formats(date, date-time, email, uri, uuid 等)。

未サポート:

  • 再帰スキーマ(自己参照 $ref)
  • 数値制約(minimum / maximum / multipleOf)
  • 文字列長制約(minLength / maxLength)
  • additionalPropertiesfalse 以外

全 object に additionalProperties: false が必須という点は忘れると地味にハマります。

Python / TypeScript SDK は未サポート制約を自動で剥がして API に送り、戻った値をクライアント側で validation する親切設計です。そのため Zod で .min(1).max(200) を書いても、送信時はスキーマから剥がされ、受信時に Zod 側で検証されます。

5. refusal の扱い

安全上の理由で Claude がスキーマ準拠の出力を拒否すると、レスポンスは stop_reason: "refusal" で返ります。このとき 出力はスキーマに合致しない可能性があるため、アプリ側で必ず分岐します。

if (res.stop_reason === "refusal") {
  // スキーマが守られない可能性あり。UI では "回答できません" 等に振る
  throw new Error("model refused to produce structured output");
}

parse() を使っていても refusal が返ったケースでは parsed_outputnull になる可能性があります。nullable を前提に ! で殴らず、ガードを書いてください。

6. 運用のメンタルモデル

  • 分類・抽出・整形が中心 → 迷わず messages.parse() + Zod / Pydantic
  • Tool の引数を信頼したい → 個別 tool に strict: true を付ける
  • 出力長・範囲を縛りたい → スキーマには書けないので system プロンプトで制約、受信側は自前で検証
  • スキーマが複雑で re-compile が多い同じスキーマは 24h キャッシュされる。同日中に同じスキーマで数万リクエスト投げても初回ペナルティは一度きり
  • citations / message prefill とは非互換 → 組み合わせると 400。citations が必要なら structured output を別リクエストに分ける

7. 落とし穴

  • output_format を使い続ける — deprecated。新規は output_config.format に寄せる
  • additionalProperties: false を書き忘れる — ほぼ必ず 400。Zod 経由なら helper が付けてくれる
  • 数値/文字列制約を書いても通らない — JSON Schema レベルでは無視される。長さ制限が欲しいなら SDK の auto-strip + client-side validate に乗るか、受信後に Zod で検証
  • refusal を null check でしか扱わない — ユーザーへのエラー表示を設計する
  • messages.parse()parsed_output! で squash — refusal や parse 失敗で null になるので ts-assert より optional-chain 推奨
  • citations を同時に有効化する — 400 が返る

まとめ

Structured Outputs は「LLM の JSON 出力を検証なしで信じてプロダクションに流す」という最大級のアンチパターンを、スキーマを 1 回書くだけで回避できる機能です。TS なら Zod、Python なら Pydantic でサーバー/クライアント両側同じ型を保ちながら使うのが主流です。

次回は Programmatic Tool CallingMemory Tool を取り上げる予定です。release notes タグで一覧を追跡できます。

参考

記事で登場したコードをひとつの最小リポジトリとしてまとめました。ファイルツリーから切り替えて、全体の構造をそのまま確認できます。

$cat./package.jsonjson
{
  "name": "claude-structured-outputs-starter",
  "private": true,
  "type": "module",
  "scripts": {
    "parse-zod": "bun src/parse-zod.ts",
    "parse-manual": "bun src/parse-manual.ts",
    "tool": "bun src/strict-tool.ts",
    "refusal": "bun src/handle-refusal.ts"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.90.0",
    "zod": "^4.3.6"
  },
  "devDependencies": {
    "@types/bun": "^1.3.0",
    "typescript": "^6.0.3"
  }
}
slugclaude-structured-outputsfiles8click a file in the tree to switch