Structured outputsのJSON Schema制限をSDKが自動変換する仕組み
structured outputsはminimum・maximumなどのJSON Schema制約に対応しません。SDKがどの制約を取り除き、descriptionへ言い換え、レスポンスをどう検証しているかを5ステップで追います。
structured outputsのJSON Schemaは、標準仕様のごく一部しか受け付けません。minimumやmaximumのような数値制約、minLengthやmaxLengthのような文字列制約を含めると、そのままでは400エラーになります。Python・TypeScript・Ruby・PHPの各SDKはこれを黙って回避します。制約をワイヤー上のスキーマから取り除き、代わりにdescriptionへ言い換えて送り、返ってきたレスポンスを元のスキーマに照らして検証します。生のJSON Schemaを直接組み立てる実装では、この安全網は働きません。この記事では、その変換が具体的に何をしているかを5つのステップに分けて追い、変換が働かない場面で何が起きるかまで扱います。
structured outputsが受け付けないJSON Schemaの制約
Claudeの応答をJSON Schemaに強制一致させるstructured outputsは、output_config.format(JSON outputs)とstrict: true(Strict tool use)という2つの仕組みの総称です。どちらも同じサブセットのJSON Schemaしか読みません。
対応する制約と対応しない制約は、次のように分かれます。
| 分類 | 制約の例 |
|---|---|
| 対応 | 制約の例type・enum・const・required・additionalProperties: false・anyOf / allOf(内部$refのみ)・default・String formats(date-time・email・uuid等)・配列minItems(0か1のみ) |
| 非対応 | 制約の例minimum / maximum / multipleOf・minLength / maxLength・再帰スキーマ・外部$ref・minItemsの2以上・additionalPropertiesのfalse以外の値 |
非対応の制約を含むスキーマをそのままAPIへ送ると、詳細付きの400エラーが返ります。回避策は2つです。制約を手作業で取り除いてdescriptionに言い換えるか、SDKに任せるかです。Pydantic・Zod経由でモデルを渡すと、後者が自動で起きます。
SDKが自動変換する5つのステップ
Python・TypeScript・Ruby・PHPの各SDKは、Pydantic・Zodなどのモデルからスキーマを生成する経路で、送信前に次の5ステップを踏みます。C#・GoのSDKも、スキーマをネイティブ型(C#のCreate<T>()、Goの構造体リフレクション)から導出する場合は同じ変換を適用します。
- 非対応の制約を取り除く(
minimum・maximum・minLength・maxLengthなど) - 制約の内容を
descriptionへ書き足す(例:「Must be at least 100」) - オブジェクトに
additionalProperties: falseを付与する - String formatを対応リストだけに絞り込む
- レスポンスを元のスキーマ(取り除く前の完全な制約)に照らして検証する
たとえばPydanticでage: int = Field(ge=0, le=150)(0以上150以下の年齢)と定義したとします。SDKが送信するワイヤー上のスキーマでは、minimum: 0とmaximum: 150は消え、ageのdescriptionに「Must be at least 0 and at most 150」といった説明文が足されます。Claudeが受け取るのはこの簡略化されたスキーマだけです。Claude自身はminimumやmaximumという制約を見ていません。descriptionの文章を手がかりに、妥当な範囲の値を返そうとしているだけです。
ここが誤解しやすい点です。ステップ5の検証はローカルで動くコードで、Claudeの推論とは無関係に走ります。SDKは元のPydanticモデル・Zodスキーマ(制約をすべて保持したもの)に対してレスポンスを検証し直します。Claudeがdescriptionの指示を外れた値を返せば、SDK側のバリデーションがそこで弾きます。「Claudeが制約を守っている」のではなく、「SDKが守らせている」というのが実態に近い表現です。
SDK別の対応状況 — 自動変換されるSDKとされないSDK
すべてのSDKが同じように振る舞うわけではありません。ネイティブなモデル型を経由するかどうかで、変換の有無が分かれます。
| SDK / 経路 | 自動変換 | 条件 |
|---|---|---|
| Python(Pydantic経由) | 自動変換あり | 条件output_configにPydanticモデルを渡す |
| TypeScript(Zod経由) | 自動変換あり | 条件zodOutputFormat()を使う |
| Ruby / PHP | 自動変換あり | 条件SDK付属のモデル記法を使う |
| C# | 自動変換あり | 条件Create<T>()でネイティブ型からスキーマを導出する場合のみ |
| Go | 自動変換あり | 条件構造体リフレクションまたはBetaJSONSchemaOutputFormat()を使う場合のみ |
| 生のJSON Schemaを直接渡す | 自動変換なし | 条件どのSDKでもtransform: false相当の経路では変換されない |
TypeScriptで生のJSON Schemaを直接使いたい場合、jsonSchemaOutputFormat()が用意されていますが、既定ではZod用と同じ変換(不要な制約の除去・additionalProperties: falseの付与・formatの絞り込み)がかかります。この変換を止めて、書いたスキーマをそのままAPIへ送りたいときは、jsonSchemaOutputFormat(schema, { transform: false })のように明示的に無効化します。無効化した場合、非対応の制約が残っていれば送信時に400エラーになります。変換に頼らず自分でスキーマを書く場合は、対応表の「非対応」列を先に消してから送る必要があります。
変換が起きない場面で何が壊れるか
自動変換はモデル経由の場合だけ働きます。生のJSON Schemaオブジェクトを組み立ててAPIへ直接渡す実装では、この安全網がありません。
よくある事故は次の3パターンです。
- 数値制約を残したまま送る:
{"type": "integer", "minimum": 0}のようなスキーマをそのままoutput_config.formatに渡すと、詳細付きの400エラーになる(公式は具体的なエラーメッセージ文字列を明記していない)。手動でdescriptionに言い換えるか、SDKのモデル経由に切り替える minItemsに2以上を指定する:配列のminItemsは0か1しか通らない。「3件以上」を強制したい場合は、descriptionで指示するか、応答後にアプリ側で件数を検証するadditionalPropertiesをtrue、または未指定にする:structured outputsのオブジェクトはadditionalProperties: falseが必須。省略すると400エラーになる
これらはいずれも仕様上の制限であり、SDKのバグではありません。制約自体を諦めるのではなく、「Claudeへの指示(description)」と「アプリ側の検証」の二段構えに分解するのが、この仕組みの前提です。
スキーマ通りにならない3つの例外
制約の変換とは別に、structured outputsそのものにもスキーマへの一致を保証しない例外が3つあります。SDKの変換ロジックを完璧に実装しても、この3つは避けられません。
- 拒否(refusal):安全上の理由でClaudeがリクエストを拒否すると、
stop_reasonはrefusalになります。ステータスコードは200で、生成されたトークン分の課金も発生しますが、応答本文はスキーマに従いません。拒否メッセージがスキーマ制約より優先されるためです max_tokens超過:出力が上限で打ち切られるとstop_reasonはmax_tokensになり、途中で終わった不完全な出力はスキーマに一致しません。対処はmax_tokensを引き上げて再試行するだけです- enum値の大文字小文字ゆれ:文字列の
enum・constは、値の大文字小文字までは保証されません。スペースの後に続く単語の先頭文字で起きやすい既知の挙動で、"Conversation Topic 3"のようなスキーマに対して"Conversation topic 3"(小文字のt)が返ることがあります。エラーにも特別なstop_reasonにもならず、応答は正常に完了します。比較は大文字小文字を無視して行い、大文字小文字の違いだけで区別されるenum値は避けるのが安全です
いずれも400エラーにはならず、stop_reasonを見て初めて気づける類の挙動です。パース処理ではstop_reasonを確認してから本文をスキーマとして扱う、という一段階を挟むと事故を防げます。
自動変換はどこまで信頼できるか
SDKの変換は便利ですが、万能ではありません。descriptionへ言い換えられた制約は、あくまでClaudeへのお願いであって、minimumやmaximumのような強制力は持ちません。Claudeが極端な値を返す確率は低いものの、ゼロではありません。ステップ5の検証があるからこそ、この仕組みは実運用に耐えます。検証を無効化したり、変換前のスキーマを信頼してSDK側のバリデーションを省いたりすると、制約は名目だけのものになります。
同じ制限はStrict tool use(strict: true)にも及びます。JSON outputsとStrict tool useは目的が違う2つの機能ですが、対応するJSON Schemaのサブセットは共通です。ツール引数のminimum・maximumも同様に非対応で、descriptionへの言い換えが必要になります。両者を同一リクエストで併用する場合、スキーマの複雑さは合算で数えられます。strict: trueのツールは1リクエスト20個まで、required外の任意プロパティは全スキーマ合計24個まで、anyOfや型配列を使う項目は16個までという上限があり、超えると「Schema is too complex for compilation」という400エラーになります。優先度の低いツールからstrictを外す、任意パラメータをrequiredに寄せる、ネストを浅くする、の順に試すのが公式の推奨です。
スキーマを変更すると、コンパイル済みのグラマー(制約付きデコーディングの内部表現)のキャッシュが無効になります。nameやdescriptionだけの変更ではキャッシュは壊れませんが、制約や型構成を変えると初回コンパイルからやり直しになり、レイテンシが増えます。頻繁にスキーマを変えるアプリでは、この再コンパイルコストも設計に織り込む必要があります。
まとめ
structured outputsのJSON Schemaは標準仕様の一部でしかなく、数値・文字列の制約は非対応です。Python・TypeScript・Ruby・PHPの各SDKはモデル経由で使う限り、この制限を5ステップの変換(除去 → description化 → additionalProperties付与 → format絞り込み → レスポンス検証)で吸収します。C#・Goもネイティブ型経由なら同じ変換が働きます。生のJSON Schemaを直接組み立てる実装では変換が働かないため、対応表の「非対応」列を自分で潰してから送る必要があります。structured outputs全体の実装手順はClaude JSONモードの使い方にまとめており、この記事はそこから「JSON Schemaの制限とSDKの変換」だけを切り出して深掘りしたものです。Strict tool useとの組み合わせ方はAnthropic Advanced Tool Use、スキーマ変更時のキャッシュ挙動はAnthropic APIのPrompt Cachingを理解する、Batch APIとの併用はClaude Batch APIの使い方で扱っています。