チュートリアル
AI Structured Output とは?JSON Schema で LLM の JSON を安定させる
プロンプトの「JSON だけ」は確率を下げるだけです。Structured Output は形をデコード段階で固定し、JSON Schema がフィールド・型・enum を契約にします。
モデルをパイプラインに繋ぐとき、怖いのは文章の拙さではなく、コードが食べられない戻り値です。カンマ欠け、フィールド名の変更、数値が文字列になる。Structured Output が解くのはそこです。モデルは token を出す時点で宣言した形で話し、あとから正規表現で JSON を削り出す前提にしません。JSON Schema はその形の最も一般的な書面です。読み終えたら、詰まっているのが「構文」「形」「業務の正否」のどれか判断でき、Schema のあともローカル検証が要ると分かるはずです。
プロンプトだけの JSON が信頼できない理由
「JSON だけ返して、説明は不要」は最もよくある場当たりです。好みを文脈に書くので効くこともあります。信頼できないのは、好みが拘束ではないからです。モデルはオブジェクトを markdown のコードフェンスで包んだり、必須キーを落としたり、priorityをより「自然な」urgencyに書き換えることがあります。下流がJSON.parseした瞬間、失敗は文言の問題ではなくパイプライン全体の停止です。
より静かな失敗は「parse できるが形が違う」です。itemsは配列のはずがオブジェクトになり、スコアは数値のはずが"0.91"になります。コードはundefinedを読むか文字列結合し、問題はかなり後ろで爆発します。プロンプトはこのドリフトを止められません。デコード段階で不正 token を拒否しないからです。
だから Structured Output を「より厳しいプロンプト」だと思わないでください。生成の一部です。サーバーが Schema を次に許される token 集合へコンパイルし、モデルは括弧の不一致や名簿にないキーを出せません。プロンプトはタスク説明、形は Schema です。
Structured Output が拘束するのはどの層か
三層で考えるとよいです。どれか二つを混ぜると「モデルが適当」に見えます。第一層は構文:parse できる JSON。第二層は形:名前・型・必須・enum が Schema に合うこと。第三層は意味:分類は正しいか、金額は本物か。Structured Output がカバーするのは前二者です。第三層は常にあなたの責任です。
| 段階 | 設定するもの | 実際の保証 |
|---|---|---|
| プロンプトの約束 | 「JSON だけ」 | 好みであり、契約ではない |
| JSON モード | MIME または json_object |
合法 JSON になる可能性は高いが、フィールド名はモデル任せ |
| Schema モード | JSON Schema と厳格フラグ | 形・型・必須・enum を Schema が拘束 |
OpenAI は Structured Outputs を JSON mode の次の段階として書いています。どちらも合法 JSON を出せますが、渡した Schema への一致を保証するのは前者だけです。公式の対照はStructured model outputs。Gemini も「JSON であればよい」と「Schema のフィールドを出す」を分けます。スイッチの詳細はGemini API の JSON 出力ガイドを見てください。本記事では SDK の細部は繰り返しません。
見落としやすい境界もあります。安全上の拒否、打ち切り、ツール呼び出し失敗は、成功オブジェクトに収まらないことがあります。API によっては別のrefusalや空の内容を返します。Schema が拘束するのは「形式で話し始めた区間」であり、「この呼び出しは必ず成功する」ではありません。
JSON Schema が契約になるまで
JSON Schema はもともと JSON 文書の語彙です。型、必須、enum、数値範囲、配列要素。書き方の標準的な解説はUnderstanding JSON Schema。モデルに繋ぐと、同じ語彙に別の仕事が加わります。事後検証だけではなく、生成中に探索空間を狭めます。
エンジニアリングでは、Schema はコンパイル時と実行時で共有する契約です。Pydantic、Zod、Swift の生成可能型は、最終的に言語非依存の JSON Schema に落ちることが多く、クラウドモデルへ渡し、ログに書き、フィクスチャで再生できます。フィールド表は一つだけ保守し、App がtotalCents、API がamount、プロンプトが「金額」と言う食い違いを避けます。
契約には「本当に読むオブジェクト」を書き、「完全な世界モデル」は書かない。任意フィールドが増えるほど、誤記入や空欄の機会が増えます。必須はrequired、閉じた集合はenum、数値範囲はminimum / maximum。プロパティのdescriptionは、プロンプトで再度説明するより安定しがちです。拘束とデコードが一体だからです。
厳格モードにはもう一つよくある規則があります。オブジェクトはadditionalProperties: falseが必要で、宣言したフィールドはすべてrequiredに入れます。本当に任意の値は「required から外す」のではなく、nullを許します。そうしないとモデルがキーを省略し、コードはobj.fieldが必ずある前提のままになります。
各 API が Schema を渡す方法
各社の製品名は Structured Output でも、包むフィールドは違います。まず二つ聞いてください。この Schema は最終回答を拘束するのか、ツール引数なのか。今のモデルスナップショットは本当に厳格モードを支援するか。ドキュメントのキーワードが全エンドポイントで同じ動きだと思わないでください。
OpenAI:json_schema と strict
Chat Completions ではresponse_formatをjson_schemaにし、strict: trueをオンにします。Responses API は同じことをテキスト形式フィールドに書きます。Schema 規則は同じで、外壳だけ違います。次のリクエストはチケットを抽出します。カテゴリは enum、アカウントは null 可です。
{
"model": "gpt-4o-2024-08-06",
"messages": [
{ "role": "system", "content": "Extract the ticket into the schema." },
{ "role": "user", "content": "Checkout 500s on a saved card. Account acct_8842." }
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "support_ticket",
"strict": true,
"schema": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"category": {
"type": "string",
"enum": ["billing", "bug", "account", "other"]
},
"severity": { "type": "integer" },
"account_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }]
}
},
"required": ["summary", "category", "severity", "account_id"],
"additionalProperties": false
}
}
}
}
応答は「全体が JSON」として扱ってください。先に正規表現でコードフェンスを削らない。SDK の parse ヘルパーは型付きオブジェクトへデシリアライズできます。拒否分岐も処理してください。安全ポリシーが発火すると、成功 Schema に合わないことがあります。
Gemini とその他のスタック
Gemini は MIME タイプで「これは JSON」と宣言し、responseSchemaまたはresponseJsonSchemaでフィールドを固定します。後者は標準 JSON Schema に近く、anyOf、$ref、数値範囲に向きます。詳細と Python / JS の例はGemini Structured Output チュートリアル。
Apple の on-device モデルは@Generableのようなコンパイル時の形であり、手書きの JSON Schema ファイルではありません。プロセスを出て HTTP を叩き、ログを書くときは、やはりシリアライズ可能な JSON が必要です。三つの呼び出しチェーンの分け方はApple AI Agent と JSON。外部 Agent が MCP や REST を使うとき、ペイロードはほぼ必ず JSON で、Schema はアダプタ層が揃える表のままです。
本番に出せる Schema
次の Schema はチケット分類を模しています。カテゴリは閉じ、優先度は整数、要約は文字列。Schema が持つべきなのは形であり、「このチケットを業務上紧急にすべきか」ではありません。
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "bug", "feature"],
"description": "Ticket category"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5
},
"summary": {
"type": "string"
}
},
"required": ["category", "priority", "summary"],
"additionalProperties": false
}
配列の要素はitemsで記述します。タプル寄りの固定長リストは JSON Schema 経路ならprefixItemsを見てください。ネストはまずインライン。同じ構造が三度目、または木ノードが自身を指すときだけ$defs + $refを使います。早すぎる抽象はエラーを読みにくくします。サーバーが Schema を拒否したとき、展開後の巨大オブジェクトと向き合うことになります。
Prompt と Schema に構造を二重に書かないでください。重複した説明はモデルを二つの話の間で揺らし、入力 token も浪費します。タスク説明はプロンプト、名前と型は Schema だけ。ローカル型がフィールドを変えたら、リクエストの Schema も一緒に変えます。そうしないと本番で新しいキーや欠けたキーが静かに現れます。
拘束したあともローカル検証は必要
Structured Output はパース層をかなり静かにします。分類が正しいことや、数値が業務上限に合うことは保証しません。enum は集合を制限できても、誤った選択は止められません。優先度 5 は合法で、「本当は 2 であるべき」も合法 JSON です。重要経路は抜検するか規則を乗せてください。
本番ではモデルの戻り値を普通の JSON として見てください。まずJSON フォーマットでネストを確認し、JSON 検証で parse できることを確かめ、同じオブジェクトをJSON Schemaに入れてローカルで再検証します。ゴールデンセットとの比較はJSON Diffで、キー順や nullable のドリフトが一目で分かります。
フィクスチャは三件残してください:ticket.valid.json、ticket.missing-field.json、ticket.wrong-enum.json。一つ目は happy path、あとの二つはローカル検証が本当に拒否することを確認します。モデル側がすでに拘束した誤りは、ローカルでも再現できるべきです。そうしないと、モデルを替える日や厳格モードを切った日に、オフラインのパイプラインで初めて気づきます。
よくある質問 FAQ
Structured Output と JSON mode の違いは?
JSON mode は parse できる JSON を保証します。Structured Output はその上で、渡した JSON Schema への一致を保証します。名前、型、必須、enum が契約どおりです。コードが名前付きフィールドを読むなら後者を使います。
Schema があるのに「JSON だけ」と書くべき?
短い一文は残してよいです。フィールド表をプロンプトに写さないでください。形は Schema が正です。二重の説明は品質を下げ、クォータも浪費します。
厳格モードで任意フィールドをどう表す?
多くの厳格実装は追加プロパティを閉じ、列挙したフィールドをすべて必須にします。任意値は nullable にします。string と null のユニオンであり、required からキーを消すのではありません。
Schema を通っても業務は間違う?
間違います。Schema は形を見ても真偽は見ません。誤分類、金額の幻覚、拒否すべきのに無理に埋める、いずれも合法 JSON になり得ます。本番では規則、抜検、人の確認が残ります。
まとめと次の一歩
Structured Output は文言の技巧ではありません。JSON の形を「希望」から「デコード拘束」へ変えます。JSON Schema はその拘束の最も一般的な書き方です。必須、enum、範囲、余分なキーの禁止が、下流が安定してJSON.parseし、期待するフィールドを読めるかを決めます。各 API のスイッチ名は違っても、層は同じです。構文、形、意味。混ぜないでください。
次は、本当に読む小さな Schema を書き、厳格モードで抽出か分類を一本通し、ブラウザで戻り値をローカル検証してください。Gemini のリクエスト例は前の記事、Agent 引数が JSON に落ちる様子は Apple の記事を開いてください。