JSON Schemaによる構造化出力:実践ガイド
プロースからJSONを解析するのはやめよう。OpenAIのstrict json_schema、GeminiのresponseSchema、Anthropicのツール使用が、定義したスキーマに一致するデータを返させる方法。
モデルの回答をコードに流し込もうとした瞬間、「JSONを返せ」だけでは不十分になります。丁寧に頼めば、謝罪でくるまれたJSON、Markdownフェンス内のJSON、構文的には有効だがパーサーが必要とするフィールドが欠けたJSONが返ってきます。信頼できる修正は、頼むのをやめて制約し始めることです。期待する正確なスキーマをプロバイダーに伝え、応答がそれに一致することを保証、あるいは少なくとも強く強制させるのです。この記事では、現在利用できる仕組み、OpenAIのstrictな json_schema、Geminiの responseSchema、Anthropicのネイティブな output_config.format(関数呼び出し側の対となるstrictなツール使用を含む)、それぞれが課すスキーマルール、そしてなお後で検証する理由を扱います。
2つの失敗モード、1つの根本原因
フリーテキストのJSONが壊れる方法は2つあり、それらを名付けると役立ちます。構文的失敗は、まったくパースできない出力です。末尾のカンマ、迷い込んだ文、コードフェンスなど。意味的失敗は、パースはできるがあなたのプログラムにとって間違っている出力です。定義しなかったenum値、欠けた必須フィールド、数値が必要なところの文字列など。プロンプトエンジニアリング(「JSONのみで応答せよ」)は最初の種類を減らし、2番目にはほとんど何もしません。スキーマ制約付き生成は両方を攻撃します。スキーマが事後にチェックされるのではなくデコード中に強制されるからです。
OpenAI:strict: true が機能のすべて
OpenAIの構造化出力は、より古いJSONモードの進化形であり、違いは1つのフラグです。素のJSONモード(response_format: { "type": "json_object" })は出力が有効なJSONであることを保証しますが、その形状については何も保証しません。構造化出力はスキーマと、モデルをそれに束縛する strict スイッチを追加します。
Chat Completionsでは json_schema 型の response_format を渡します。
from openai import OpenAI
client = OpenAI(base_url="https://your-gateway.example.com/v1", api_key="AVE_GATEWAY_API_KEY")
resp = client.chat.completions.create(
model="gpt-5.6",
messages=[{"role": "user", "content": "Extract the invoice fields."}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "invoice",
"strict": True,
"schema": {
"type": "object",
"properties": {
"vendor": {"type": "string"},
"total": {"type": "number"},
"currency": {"type": "string", "enum": ["USD", "EUR", "JPY"]},
},
"required": ["vendor", "total", "currency"],
"additionalProperties": False,
},
},
},
)
Responses API では同じスキーマが別のフィールド、つまり response_format ではなく text.format に移動します。通常の responses.create 呼び出しにはトップレベルの output_parsed はありません。パース済みオブジェクトを得るには、型付きフォーマットとともにSDKの parse ヘルパーを使うか、response.output_text を読み取って自分で検証します。
from pydantic import BaseModel
class Invoice(BaseModel):
vendor: str
total: float
currency: str
resp = client.responses.parse(
model="gpt-5.6",
input="Extract the invoice fields.",
text_format=Invoice, # SDK builds the strict json_schema and parses the result
)
invoice = resp.output_parsed # a validated Invoice instance
生の経路を使いたい場合は、responses.create に text={"format": {"type": "json_schema", "name": "invoice", "strict": True, "schema": SCHEMA}} を送り、resp.output_text を明示的にパースして検証してください。パース済みフィールドが自動で現れるとは考えないでください。
Responsesに response_format を送る、あるいはChat Completionsに text.format を送るのは、頻繁で紛らわしいエラーです。変わったのはフィールド名であって概念ではありません。ゲートウェイの対応範囲はアダプターごとに異なり、一様ではありません。このゲートウェイはOpenAI形式の response_format をGeminiの responseSchema にマッピングしますが、Claudeのネイティブな構造化出力フィールドには現在変換しません。そのためClaudeには、response_format がそのまま届くと仮定せず、下記のAnthropicネイティブ形式を送ってください。
strictスキーマのサブセットにはルールがある
strict: true はJSON Schemaの多くをサポートしますが、すべてではなく、制約は十分に厳しいため、通常のバリデーターで問題なく検証されるスキーマがここでは拒否されることがあります。
- すべてのプロパティを
requiredに列挙しなければならない。 真にオプションのフィールドはありません。「欠けているかもしれない」をモデル化するには、requiredから省くのではなく、フィールドをnullable union、すなわち"type": ["string", "null"]にします。 additionalProperties: falseはすべてのオブジェクトで必須。 モデルは余分なキーを発明できません。- ルートはオブジェクトでなければならない。 トップレベルの配列は許可されません。配列型のプロパティを持つオブジェクトでラップします。
- 再帰は
{"$ref": "#"}でルートを参照して使います。これがネストしたツリー構造を表現する方法です。
モデルが安全上の理由で拒否する場合、それはスキーマ形状のコンテンツではなくメッセージ上の refusal フィールドとしてプログラム的に表面化するため、パースを信頼する前に拒否をチェックします。また、応答が長さで打ち切られることもあります。finish_reason を検査してください("length" の値は打ち切られたJSONを意味します)。スキーマが強制されていても、打ち切られたstrict応答はやはり無効なJSONだからです。
Gemini:responseSchema とJSON mimeタイプ
Geminiは generationConfig を通じて出力を制約します。responseMimeType を "application/json" に設定し、responseSchema(OpenAPIスタイルのスキーマオブジェクト)を提供します。あるいは標準JSON Schemaの場合は responseJsonSchema を使います。
from google import genai
from google.genai import types
client = genai.Client()
resp = client.models.generate_content(
model="gemini-3.5-flash",
contents="Classify the ticket.",
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema={
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "bug", "feature"]},
"priority": {"type": "integer"},
},
"required": ["category", "priority"],
},
),
)
Geminiは生成をスキーマに制約し、分類のための enum をサポートし、propertyOrdering でフィールド順を固定できます。順序が品質と一貫性に影響しうるため有用です。ドキュメントが明示している2つの注意点に留意してください。すべてのJSON Schema機能がサポートされているわけではなく、非常に複雑なスキーマは拒否されることがあります。Geminiはまた、出力が有効なスキーマ形状のJSONである一方で、なお値を検証すべきだと強調しています。スキーマに準拠しつつ意味的に間違ったもの(もっともらしいが不正確なカテゴリ)を生成しうるからです。この仕組みは形状を保証するのであって、真実を保証するのではありません。
Anthropic:ネイティブな output_config.format とstrictなツール使用
Anthropicは現在、ネイティブな構造化出力を提供しているため、強制ツール使用は唯一の方法ではなく、互換性のためのフォールバックになりました。2つの補完的な機能があります。JSON出力では、リクエストに output_config オブジェクトを置き、その format を {"type": "json_schema", "schema": ...} とします。Claudeの回答はテキストコンテンツ内に、スキーマに一致するJSONとして返ります。strictなツール使用では、ツールの input_schema に strict: true を追加し、スキーマに合うツール引数を保証します。どちらか一方だけでも、両方を組み合わせても使えます。
import anthropic
client = anthropic.Anthropic(base_url="https://your-gateway.example.com", api_key="AVE_GATEWAY_API_KEY")
msg = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {"vendor": {"type": "string"}, "total": {"type": "number"}},
"required": ["vendor", "total"],
"additionalProperties": False,
},
}
},
messages=[{"role": "user", "content": "Extract the invoice fields."}],
)
data = msg.content[0].text # valid JSON matching your schema
現在の制限も押さえておく必要があります。additionalProperties は false でなければならず、minimum や maxLength などの数値・文字列制約と再帰スキーマはまだサポートされていません。送ると 400 が返ります。Python SDKは client.messages.parse(..., output_format=PydanticModel) も公開しており、スキーマの構築と回答のパースを1ステップで行えます。モデルに何かを明確に呼び出させる必要がある場合は、strictなツール使用が慣用的な方法です。目的のスキーマを input_schema に持つツールを定義し、strict: true を追加して、tool_choice で強制します。
msg = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=[{
"name": "record_invoice",
"strict": True,
"input_schema": {
"type": "object",
"properties": {"vendor": {"type": "string"}, "total": {"type": "number"}},
"required": ["vendor", "total"],
"additionalProperties": False,
},
}],
tool_choice={"type": "tool", "name": "record_invoice"},
messages=[{"role": "user", "content": "Extract the invoice fields."}],
)
data = next(b.input for b in msg.content if b.type == "tool_use")
ツールを強制すると決定論的になります。モデルは record_invoice を呼び出さなければならず、その input があなたのオブジェクトです。型付きの回答が欲しいときはJSON出力を、保証された有効な引数でモデルに何かを呼び出させるときはstrictなツール使用を選びます。strict なしの強制ツール使用は、古いモデルや一部のツールループ形式向けの従来型フォールバックとして残りますが、現行モデルではネイティブ機能を優先してください。製品がすでにプロバイダー間でツールをルーティングしているなら、その定義を引き継げます。詳しくは プロバイダー間でのポータブルなツール呼び出し を参照してください。
構造化出力 vs 関数呼び出し:意図で選ぶ
JSON Schemaのこれら2つの用途は似て見え、混同しやすいものです。区別は意図にあります。モデルが何かを行うツール、たとえばデータベースへのクエリ、APIの呼び出し、コードの実行を、呼び出すべきか、どう呼び出すかを決めるべきときは関数呼び出しを使います。プログラムが消費するためにモデルの回答を単にデータとして形作りたいだけのときは構造化レスポンスフォーマット(json_schema / text.format / responseSchema / output_config.format)を使います。現在は3社すべてが両方を公開しています。OpenAIはレスポンスフォーマットとツールの双方でstrictスキーマをサポートし、AnthropicはネイティブJSON出力とstrictなツール使用を組み合わせ、Geminiはレスポンススキーマと関数宣言を別々に提供します。型付き回答だけが欲しいのにツール呼び出しに手を伸ばすと、必要のない往復が増えます。
それでも検証する — 常に
スキーマの強制は検証を省く許可証ではありません。3つの具体的な理由があります。第一に、強制は形状を保証するのであって、決して正しさを保証しません。モデルは正しく型付けされているが事実として間違った値を返しうるもので、それを捕まえるのはあなたのビジネスルールだけです。第二に、打ち切りは起こります。長さ制限やタイムアウトした応答は不完全なJSONを生み、それはどのスキーマモードでも救えないため、finish_reason/status とパースエラーを処理しなければなりません。第三に、スキーマのサポートはプロバイダーごとに異なるサブセットであるため、ある上流で厳密に強制されるスキーマが別の上流ではより緩く扱われることがあります。パースされたオブジェクトを境界でバリデーター(Pydantic、Zod、JSON Schema)に通し、検証の失敗を例外ではなく通常の予期された分岐として扱いましょう。これは、あらゆる外部入力を制限することの背後にある同じ多層防御の原則です。
ここでゲートウェイは、response_format を送る1つの場所と、どのリクエストが構造化出力を使い何回リトライされたかを見る1つの場所を、すべてのクライアントに3つのプロバイダーのフィールド名を教えることなく提供して役立ちます。OpenAI互換APIゲートウェイ の背後では、OpenAI形式の response_format を一度書けば、ゲートウェイがそれをモデルを提供するチャネルへルーティングします。ただし、変換はアダプター固有です。OpenAIにはそのまま届き、Geminiの responseSchema にはマッピングされますが、Claudeのネイティブな output_config.format は現在そこから生成されません。そのためClaudeにはAnthropicネイティブ形式を使ってください。
FAQ
JSONモードと構造化出力の違いは何ですか?
JSONモード(response_format: {"type": "json_object"})は出力がJSONとしてパースされることを保証しますが、その形状については何も言いません。フィールド、型、enumは制約されません。構造化出力は strict: true の json_schema を追加し、モデルをあなたのスキーマに束縛します。プロバイダーがサポートするなら常に構造化出力を優先し、素のJSONモードは本当に固定の形状を持たないケースのために取っておきましょう。
他所では検証できるのに、なぜ私のstrictスキーマは拒否されるのですか?
strictサブセットは完全なJSON Schemaより狭いのです。通常の犯人は、欠けた additionalProperties: false、required から漏れたプロパティ、または非オブジェクトのルートです。すべてのフィールドを必須にし(オプションのものにはnullable unionを使う)、各オブジェクトに additionalProperties: false を設定し、トップレベルの配列をオブジェクトでラップしてください。Claudeでは特に、数値・文字列制約と再帰スキーマも明確に拒否されるため、それらも取り除いてください。
構造化出力は値が正しいことを保証しますか?
いいえ。応答がスキーマの形状、すなわち型、必須フィールド、許可されたenum値に一致することを保証するのであって、内容が正確であることは保証しません。モデルは完璧に型付けされているが間違った回答を返しうります。パース後は常に値をビジネスルールに照らして検証し、スキーマ準拠を正しさの保証ではなく構文の保証として扱いましょう。
Claudeからスキーマ制約付き出力を得るにはどうすればよいですか?
型付きの回答には、format が {"type": "json_schema", "schema": ...} の output_config を送ります。モデルに何かを呼び出させる場合は、強制ツールに strict: true を追加します。どちらも現行モデルのネイティブ機能です。strict なしでツールを強制する古い方法も動作しますが、現在は意図された経路ではなくフォールバックです。
正規表現を1つ削除する
コードの中で現在モデルのフリーテキスト回答をパースして正規表現でJSONを抜き出している箇所を1つ取ってください。プロンプトを json_schema(またはClaudeの output_config.format)に置き換え、strict: true を設定し、パースされた結果をスキーマバリデーターで包みます。それから意図的に難しい入力を送り、2つのことを確認してください。リクエストがフェンス付きJSONのクリーンアップ手順なしで成功すること、そして拒否や打ち切りがクラッシュではなく処理済みの分岐として表面化することです。その正規表現を取り除くことが、制約が仕事をしている具体的な信号です。