Claude Media
Structured outputsのJSON Schema制限をSDKが自動変換する仕組み

Structured outputsのJSON Schema制限をSDKが自動変換する仕組み

structured outputsはminimum・maximumなどのJSON Schema制約に対応しません。SDKがどの制約を取り除き、descriptionへ言い換え、レスポンスをどう検証しているかを5ステップで追います。

structured outputsのJSON Schemaは、標準仕様のごく一部しか受け付けません。minimummaximumのような数値制約、minLengthmaxLengthのような文字列制約を含めると、そのままでは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しか読みません。

対応する制約と対応しない制約は、次のように分かれます。

分類制約の例
対応制約の例typeenumconstrequiredadditionalProperties: falseanyOf / allOf(内部$refのみ)・default・String formats(date-timeemailuuid等)・配列minItems(0か1のみ)
非対応制約の例minimum / maximum / multipleOfminLength / maxLength・再帰スキーマ・外部$refminItemsの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の構造体リフレクション)から導出する場合は同じ変換を適用します。

  1. 非対応の制約を取り除く(minimummaximumminLengthmaxLengthなど)
  2. 制約の内容をdescriptionへ書き足す(例:「Must be at least 100」)
  3. オブジェクトにadditionalProperties: falseを付与する
  4. String formatを対応リストだけに絞り込む
  5. レスポンスを元のスキーマ(取り除く前の完全な制約)に照らして検証する

たとえばPydanticでage: int = Field(ge=0, le=150)(0以上150以下の年齢)と定義したとします。SDKが送信するワイヤー上のスキーマでは、minimum: 0maximum: 150は消え、ageのdescriptionに「Must be at least 0 and at most 150」といった説明文が足されます。Claudeが受け取るのはこの簡略化されたスキーマだけです。Claude自身はminimummaximumという制約を見ていません。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つは避けられません。

  1. 拒否(refusal):安全上の理由でClaudeがリクエストを拒否すると、stop_reasonrefusalになります。ステータスコードは200で、生成されたトークン分の課金も発生しますが、応答本文はスキーマに従いません。拒否メッセージがスキーマ制約より優先されるためです
  2. max_tokens超過:出力が上限で打ち切られるとstop_reasonmax_tokensになり、途中で終わった不完全な出力はスキーマに一致しません。対処はmax_tokensを引き上げて再試行するだけです
  3. enum値の大文字小文字ゆれ:文字列のenumconstは、値の大文字小文字までは保証されません。スペースの後に続く単語の先頭文字で起きやすい既知の挙動で、"Conversation Topic 3"のようなスキーマに対して"Conversation topic 3"(小文字のt)が返ることがあります。エラーにも特別なstop_reasonにもならず、応答は正常に完了します。比較は大文字小文字を無視して行い、大文字小文字の違いだけで区別されるenum値は避けるのが安全です

いずれも400エラーにはならず、stop_reasonを見て初めて気づける類の挙動です。パース処理ではstop_reasonを確認してから本文をスキーマとして扱う、という一段階を挟むと事故を防げます。

自動変換はどこまで信頼できるか

SDKの変換は便利ですが、万能ではありません。descriptionへ言い換えられた制約は、あくまでClaudeへのお願いであって、minimummaximumのような強制力は持ちません。Claudeが極端な値を返す確率は低いものの、ゼロではありません。ステップ5の検証があるからこそ、この仕組みは実運用に耐えます。検証を無効化したり、変換前のスキーマを信頼してSDK側のバリデーションを省いたりすると、制約は名目だけのものになります。

同じ制限はStrict tool use(strict: true)にも及びます。JSON outputsとStrict tool useは目的が違う2つの機能ですが、対応するJSON Schemaのサブセットは共通です。ツール引数のminimummaximumも同様に非対応で、descriptionへの言い換えが必要になります。両者を同一リクエストで併用する場合、スキーマの複雑さは合算で数えられます。strict: trueのツールは1リクエスト20個まで、required外の任意プロパティは全スキーマ合計24個まで、anyOfや型配列を使う項目は16個までという上限があり、超えると「Schema is too complex for compilation」という400エラーになります。優先度の低いツールからstrictを外す、任意パラメータをrequiredに寄せる、ネストを浅くする、の順に試すのが公式の推奨です。

スキーマを変更すると、コンパイル済みのグラマー(制約付きデコーディングの内部表現)のキャッシュが無効になります。namedescriptionだけの変更ではキャッシュは壊れませんが、制約や型構成を変えると初回コンパイルからやり直しになり、レイテンシが増えます。頻繁にスキーマを変えるアプリでは、この再コンパイルコストも設計に織り込む必要があります。

まとめ

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の使い方で扱っています。

この記事を共有:XはてブLinkedIn