OpenAI APIの構造化出力の使い方|JSON Schemaで固定する手順

OpenAI APIの構造化出力の使い方|JSON Schemaで固定する手順
📅 公開日: 2026年9月17日🔄 最終更新: 2026年10月2日

OpenAI APIが返したJSONをそのままデータベースやワークフローへ流したとき、キーが1つ欠けただけで後段の処理が止まる。この事故を仕組みで防ぐのが構造化出力(Structured Outputs)です。公式ドキュメントは、この機能を「渡したJSON Schemaに常に従った応答を生成するため、必須キーの欠落や未定義のenum値の作り込みを心配しなくてよくなる」ものだと説明しています(OpenAI公式ドキュメント「Structured model outputs」)。

先に結論を書きます。後段の処理を壊さないために必要なのは、次の流れです。第一に、受け取る側のデータ構造からJSON Schemaを先に決める。第二に、Responses APIのtext.formatにjson_schemaとstrict: trueを指定して渡す。第三に、スキーマに従わない応答(refusalと出力の打ち切り)だけを例外として分岐させる。この3段構えにすると、パースの失敗を握りつぶすリトライ処理が不要になります。

この記事は、スキーマの設計からAPIへの渡し方、strictモードが課す条件、使える型と使えないキーワード、上限値、失敗時の分岐までを手順の順番どおりに並べています。ブラウザ版のChatGPTしか触ったことがない方は、先にChatGPTの使い方でブラウザ版の基本操作を押さえておくと、本記事のどこがコード側の話なのかを区別しやすくなります。

目次

OpenAI APIの構造化出力とは|JSON Schemaどおりの応答を返させる機能

【完全無料】AI×SNSで成果を出す100大特典を受け取る

AIの使い方を、SNS運用の実務につなげたい方へ。LINEで特典の案内を受け取れます。

【完全無料】AI×SNSで成果を出す100大特典を受け取る

構造化出力(Structured Outputs)とは、開発者が渡したJSON Schemaに従った応答をモデルに生成させるOpenAI APIの機能で、公式は型安全性・拒否の明示・プロンプト簡素化の3点を利点として挙げています。

公式ドキュメントが挙げる利点は3つで、いずれも後段のコード量に直結します。1つ目は「不正な形式の応答を検証したりリトライしたりする必要がない」型安全性、2つ目は「安全上の理由による拒否をプログラムから検知できる」明示的なrefusal、3つ目は「一貫した書式を得るために強い言い回しのプロンプトを書かなくてよい」プロンプトの簡素化です(Structured model outputs)。

読者がここで決めるのは「自分のユースケースが構造化出力の対象かどうか」です。判断の基準は単純で、モデルの出力を人間が読むだけなら不要、出力をコードが読んで次の処理へ渡すなら対象になります。問い合わせメールの要約を担当者が目で読むだけなら通常のテキスト応答で足ります。一方、要約結果を「緊急度」「担当部署」「返信期限」に分解してチケット管理へ登録するなら、キーの欠落がそのまま登録エラーになるため構造化出力の対象です。

仮の例として、問い合わせメールを分類する最小構成を挙げます。スキーマはcategory(enum)とsummary(文字列)の2キーだけ、指定はstrict: true。このとき返ってくるのは、次のような形です。

{
  "category": "billing",
  "summary": "先月分の請求額が二重に引き落とされているという申告"
}

enumにbilling、technical、otherの3語だけを許可しておけば、モデルが「請求関連」のような未定義の文字列を返す経路がなくなります。後段のswitch文にdefault分岐を書いて未知の値を握りつぶす、という保険が不要になるのが、この機能の実利です。

構造化出力とJSONモードの違いはどこにあるか

JSONモード(json_object)は出力が有効なJSONであることだけを担保し、構造化出力(json_schema)はそれに加えてスキーマへの準拠まで担保します。公式は可能な限り構造化出力を使うことを推奨しています。

公式ドキュメントは構造化出力を「JSONモードの発展形」と位置づけ、両者とも有効なJSONを出力する点は同じだが、スキーマ準拠を保証するのは構造化出力だけだと明記しています。そのうえで「可能な場合はJSONモードではなく常に構造化出力を使うことを推奨する」としています。次の表は、公式の比較表の記述を実装判断の形に並べ直したものです(出典:Structured model outputs)。

確認したい点 構造化出力(json_schema)はどうか JSONモード(json_object)はどうか
出力は有効なJSONになるか なる なる(公式が挙げる一部のエッジケースは自分で検知して処理する)
スキーマどおりのキーが揃うか 揃う(サポート対象のスキーマ記法の範囲内) 揃わない。特定のスキーマとの一致は保証されない
有効化はどう書くか text: { format: { type: "json_schema", "strict": true, "schema": ... } } text: { format: { type: "json_object" } }
プロンプト側に追加の義務はあるか ない ある。会話のどこかでJSONを生成せよと明示しないとエラーになる
使えるAPIはどこか Responses、Chat Completions、Assistants、Fine-tuning、Batch 同じ5つのAPIで利用できる

JSONモード側の「プロンプトに明示する義務」は見落としやすい落とし穴です。公式は、JSON生成の指示を入れ忘れると「モデルが終わりのない空白を生成し続け、トークン上限に達するまでリクエストが走り続けることがある」と警告し、文脈中に「JSON」という文字列が現れない場合はAPIがエラーを返すと説明しています。仮の例として、JSONモードでsystemメッセージへ「JSON形式で答えてください」の一文を入れ忘れると、リクエストは正常終了せず課金対象のトークンだけが増える、という結果になります。

読者がここで決めるのは「旧来のjson_objectを残すか、json_schemaへ移すか」です。既存コードにJSONパースのtry-catchとリトライが書いてあるなら、それはJSONモード前提の設計です。スキーマを1つ定義してjson_schemaへ移すと、そのtry-catchは打ち切りとrefusalの2ケースだけに縮みます。

function callingとtext.formatのどちらで構造化するか

構造化出力はfunction callingとjson_schemaレスポンス形式の2つの形で使えます。公式の分岐は、モデルを外部の関数やデータに接続するならfunction calling、ユーザーへの応答そのものを構造化するならtext.formatです。

公式ドキュメントは、この2つを目的で分けています。「モデルをシステム内のツールや関数、データなどに接続するならfunction callingを使うべき」「モデルがユーザーへ応答するときの出力を構造化したいなら、構造化されたtext.formatを使うべき」という書き分けです(Structured model outputs(developers.openai.com))。例として、注文を照会する関数をモデルに呼ばせるのは前者、数式の解法を段階ごとに分けて画面へ描画するのは後者にあたる、と説明されています。

仮の例で差を見ます。同じ「注文情報」を扱っても、目的が違えば書く場所が変わります。社内の注文DBを叩かせたいなら、関数定義のparametersにorder_idを置き、strict: trueを付けてモデルに引数を組み立てさせます。逆に、問い合わせ本文から注文情報を抜き出して画面に表形式で出したいだけなら、関数は不要でtext.formatに抽出用スキーマを渡します。前者はモデルが「呼び出す」、後者はモデルが「答える」――この一語の違いが分岐点です。

なお、どちらのAPIを叩くか自体で迷っている場合は、機能比較の前にエンドポイントの選択を済ませてください。Responses APIとChat Completionsの違いと使い分けで、会話状態の持ち方やツール利用の前提が整理されています。本記事のコード例はResponses APIのtext.formatを前提に書いています。

構造化出力に対応しているモデルを確認する手順

モデルが構造化出力に対応しているかは、公式モデルページの「Supported features」にstructured_outputsが含まれているかで確認できます。2026年10月2日時点の公式ガイドは、新規プロジェクトの起点として現行のgpt-6-astraを挙げています。

確認手順は3ステップです。まずモデル一覧ページで候補を絞り、次に個別のモデルページを開いて「Supported features」にstructured_outputsがあるかを見て、最後に「Endpoints」で自分が使うエンドポイント(v1/responsesかv1/chat/completions)がSupportedかを確認します。公式ドキュメントは構造化出力について「最新の大規模言語モデルで利用できる。新規プロジェクトはgpt-6-astraから始めるとよい。gpt-4-turbo以前の旧モデルは代わりにJSONモードを使うことがある」と説明しています(Structured model outputs(developers.openai.com)、2026年10月2日確認)。つまり、モデル名は世代が進むたびに入れ替わるため、記事やサンプルコードのmodel指定をそのまま写さず、実装時点の公式ページで確かめる運用が前提になります。

次の表は2026年10月2日に公式の各モデルページで確認した内容です。7件とも「Supported features」にstructured_outputsが載っており、Chat CompletionsとResponsesとBatchが「Supported」、Assistantsが「Not supported」と表示されていました。位置づけの列は、OpenAIの公式モデル案内の記述に沿っています。API料金はトークン従量制でモデルごとに異なり、Batchなどの割引条件も別に定められているため、この表には単価を載せていません。最新の単価と適用条件はOpenAI公式の料金ページ(2026年10月2日確認)で確認してください。

モデルID structured_outputsに対応しているか 使えるエンドポイントはどれか 公式での位置づけ 確認元の公式ページ
gpt-6-astra 対応(Supported featuresに記載) ResponsesとChat Completionsの両方 複雑な作業向けの最上位モデル。構造化出力ガイドの新規プロジェクト向け起点 GPT-6 Astra
gpt-6.1-sol 対応(Supported featuresに記載) ResponsesとChat Completionsの両方 Astraに近い性能を、Astraより低いコストで使うことを想定したモデル GPT-6.1 Sol
gpt-6-luna 対応(Supported featuresに記載) ResponsesとChat Completionsの両方 要約・抽出など、絞った高頻度タスク向けの効率重視モデル GPT-6 Luna
gpt-5.6-sol 対応(Supported featuresに記載) ResponsesとChat Completionsの両方 GPT-5.6ファミリーの旗艦(gpt-5.6エイリアスの向き先) GPT-5.6 Sol
gpt-5.6-terra 対応(Supported featuresに記載) ResponsesとChat Completionsの両方 性能とコストのバランスを取ったGPT-5.6ファミリーのモデル GPT-5.6 Terra
gpt-5.6-luna 対応(Supported featuresに記載) ResponsesとChat Completionsの両方 コスト重視・大量処理向けのGPT-5.6ファミリーのモデル GPT-5.6 Luna
gpt-5.5(旧モデル) 対応(Supported featuresに記載) ResponsesとChat Completionsの両方 前世代の旗艦。ChatGPT・ChatGPT Work・Codexでは2026年10月14日に提供終了と案内され、OpenAI APIでは引き続き利用可能とされている GPT-5.5

GPT-5.5の提供終了日はChatGPTとCodex側の案内(OpenAI公式のモデル案内、2026年10月2日確認)に基づくもので、APIでの扱いは別に確認が必要です。既存コードがGPT-5.5やそれ以前のモデルIDを固定している場合は、現行モデルへ差し替えたうえでスキーマを再検証してください。

読者がここで決めるのは「抽出タスクにどのモデルを割り当てるか」です。仮の例として、日次で大量の問い合わせを3カテゴリに分類するだけの処理なら、推論の難度が低いので効率重視のLuna系を候補にし、契約書から条項を抜き出して差分を判定するような処理は上位のAstraやSolを候補にする、という配分になります。いずれの場合も、実データ数十件で出力を比べてから決めるのが確実です。モデルの世代ごとの得意分野はChatGPT最新モデルの違いと使い分け、旗艦モデルの位置づけはGPT-6 Astraとはが参考になります。なお、ここで扱ったのはAPIの従量課金であり、ChatGPTの月額プランとは別会計です。プラン側の費用感はChatGPT料金プラン完全比較に分けて書いています。

ステップ1:後段処理から逆算してJSON Schemaを設計する

スキーマ設計とは、モデルに守らせるJSONの型・キー名・必須項目を先に確定させる作業です。公式はキー名を明確に付けること、重要なキーにtitleとdescriptionを付けること、evalsで最適な構造を確かめることを推奨しています。

設計の起点はモデル側ではなく、受け取る側のテーブル定義やAPIのリクエストボディです。公式ドキュメントは生成品質を上げる工夫として「キー名を明確かつ直感的に付ける」「構造上重要なキーには明快なtitleとdescriptionを付ける」「自分のユースケースに最適な構造を判断するためにevalsを作って使う」を挙げています(Structured model outputs)。descriptionは飾りではなく、モデルへの指示そのものとして働きます。

仮の例として、問い合わせメールから「緊急度」「担当部署」「要約」を取り出すスキーマを書き上げた状態を示します。urgencyにenumを置いて表記ゆれを閉じ、departmentのdescriptionに判断基準を書き込むのがポイントです。

{
  "type": "object",
  "properties": {
    "urgency": {
      "type": "string",
      "description": "対応の緊急度",
      "enum": ["high", "normal", "low"]
    },
    "department": {
      "type": "string",
      "description": "一次対応する部署。支払いに関する内容はbilling、動作不具合はsupport、それ以外はgeneral",
      "enum": ["billing", "support", "general"]
    },
    "summary": {
      "type": "string",
      "description": "本文の要点を120文字以内で説明した日本語のテキスト"
    }
  },
  "required": ["urgency", "department", "summary"],
  "additionalProperties": false
}

ここでの判断は「どこまでenumで閉じるか」です。値の集合が決まっていて増減が少ない項目はenumにし、自由記述はdescriptionで長さと言語を縛る。逆に、部署コードが頻繁に増える運用ならenumは保守負担になるため、文字列のまま受け取って後段でマスタ照合する設計が向きます。指示文そのものの書き方に不安があるなら、ChatGPTのプロンプトはなぜ伝わらない?書き方の5つのコツの考え方がdescriptionにもそのまま使えます。

ステップ2:text.formatにjson_schemaを指定してAPIへ渡す

Responses APIで構造化出力を有効にするには、リクエストのtext.formatにtypeとしてjson_schema、スキーマ名のname、JSON Schema本体のschema、そしてstrictを指定します。

公式ドキュメントが示す指定の形はtext: { format: { type: "json_schema", "strict": true, "schema": ... } }です。実際のリクエストではnameも併せて渡します(Structured model outputs)。次は、公式のcurl例と同じ構造で、ステップ1のスキーマを載せた仮の例です。

curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.6-luna",
    "input": [
      { "role": "system", "content": "問い合わせ本文を指定の構造へ変換してください。" },
      { "role": "user", "content": "先月の請求が二重に引き落とされています。至急確認をお願いします。" }
    ],
    "text": {
      "format": {
        "type": "json_schema",
        "name": "inquiry_triage",
        "schema": {
          "type": "object",
          "properties": {
            "urgency": { "type": "string", "enum": ["high", "normal", "low"] },
            "department": { "type": "string", "enum": ["billing", "support", "general"] },
            "summary": { "type": "string" }
          },
          "required": ["urgency", "department", "summary"],
          "additionalProperties": false
        },
        "strict": true
      }
    }
  }'

実装時に知っておきたい挙動が1つあります。公式は「あるスキーマで最初に送るリクエストは、APIがスキーマを処理する分の追加レイテンシが発生するが、同じスキーマでの後続リクエストには追加のレイテンシは発生しない」と注記しています。バッチ処理の先頭1件だけ遅いのは仕様であり、タイムアウト値を先頭基準で決めると無駄に長くなります。

読者がここで決めるのは「スキーマをリクエストごとに組み立てるか、固定して使い回すか」です。固定して使い回す方が上記のレイテンシの恩恵を受けられます。リクエストの組み立てからテストまでをエディタ上で任せたい場合は、OpenAI Codex 使い方で導入手順を確認しておくと、スキーマの変更と呼び出しコードの追随を同じ場所で回せます。

strict: trueが課す2つの必須条件と任意項目の書き方

strictモードは構造化出力の仕組みを使って関数呼び出しやレスポンスをスキーマへ厳密に一致させる設定で、有効にするとadditionalProperties: falseの明示と全フィールドのrequired指定が要求されます。

公式のfunction callingガイドは「strictをtrueにすると、ベストエフォートではなく確実にスキーマへ従った関数呼び出しになる。strictモードは常に有効化することを推奨する」と述べ、その内部実装が構造化出力であるため次の2つの要件が生じると説明しています。あわせて、任意項目の書き方も補足しています(OpenAI公式ドキュメント「Function calling」)。

  • parameters内のすべてのオブジェクトでadditionalPropertiesをfalseに設定する
  • propertiesのすべてのフィールドをrequiredに指定する
  • (必須条件ではなく公式の補足)省略可能な項目は、型の選択肢にnullを加えて表現できる

3つ目の「nullで省略可を表現する」は、構造化出力のガイドでも同じ扱いです。全フィールドがrequiredでもモデルは各パラメータに値を返すため、ユニオン型のnullを使えば任意項目を模倣できる、と書かれています。仮の例として、単位が未指定でもよい関数引数は次の形になります。

{
  "type": "function",
  "name": "get_weather",
  "description": "Fetches the weather in the given location",
  "strict": true,
  "parameters": {
    "type": "object",
    "properties": {
      "location": { "type": "string", "description": "The location to get the weather for" },
      "unit": {
        "type": ["string", "null"],
        "description": "The unit to return the temperature in",
        "enum": ["F", "C"]
      }
    },
    "additionalProperties": false,
    "required": ["location", "unit"]
  }
}

省略時の既定値にも注意が必要です。公式は「strict: trueを送ってスキーマが要件を満たさない場合、不足している制約の詳細とともにリクエストは拒否される」とし、さらに「strictを省略した場合の既定はAPIによって異なる。Responsesは可能な限りスキーマを厳密モードへ正規化しようとし、互換にできなければベストエフォートの非厳密な関数呼び出しへフォールバックする。フォールバックが起きるとレスポンスのツールにはstrict: falseが表示される。Chat Completionsは既定で非厳密のまま」と明記しています。つまり、明示的に書かないと挙動がAPIごとに割れます。

使える型と、値を絞り込める制約キーワード

構造化出力はJSON Schemaのサブセットに対応しており、String・Number・Boolean・Integer・Object・Array・Enum・anyOfの各型と、文字列・数値・配列に対する一部の制約キーワードが使えます。

対応範囲は公式ドキュメントの「Supported schemas」に列挙されています。型はString、Number、Boolean、Integer、Object、Array、Enum、anyOfの8種類。加えて、型ごとに値を絞り込むキーワードが用意されています。次の表は、その制約が後段のどの不具合を潰すのかまで対応づけたものです(出典:Structured model outputs)。

制約をかける対象 指定できるキーワードは何か その指定で防げる後段の不具合は何か
文字列(string) pattern(正規表現に一致させる)、format IDや識別子の書式ゆれによる照合失敗
文字列の書式(format) date-time、time、date、duration、email、hostname、ipv4、ipv6、uuid 日付やメールアドレスのパース時例外
数値(number) multipleOf、maximum、exclusiveMaximum、minimum、exclusiveMinimum 想定外の範囲の値がそのまま集計へ混入すること
配列(array) minItems、maxItems 空配列や過大な件数による画面崩れとタイムアウト

仮の例として、ユーザー情報を受け取るスキーマに制約を入れた完成形を示します。usernameは先頭が@である正規表現、emailはformatで検証、scoreは範囲で縛っています。

{
  "name": "user_data",
  "strict": true,
  "schema": {
    "type": "object",
    "properties": {
      "name": { "type": "string", "description": "The name of the user" },
      "username": {
        "type": "string",
        "description": "The username of the user. Must start with @",
        "pattern": "^@[a-zA-Z0-9_]+$"
      },
      "email": { "type": "string", "format": "email" },
      "score": { "type": "number", "minimum": 0, "maximum": 100 }
    },
    "additionalProperties": false,
    "required": ["name", "username", "email", "score"]
  }
}

ただしこれらの制約には例外があります。公式は、ファインチューニング済みモデルでは文字列のminLength・maxLength・pattern・format、数値のminimum・maximum・multipleOf、オブジェクトのpatternProperties、配列のminItems・maxItemsが未対応だと明記しています。ファインチューニングを併用する計画があるなら、制約はスキーマ側ではなくアプリケーション側のバリデーションに置く判断になります。

もう1つ実務で効く挙動として、キーの並び順があります。公式は「構造化出力を使うと、出力はスキーマのキーの順序と同じ順序で生成される」と書いています。理由づけを先に書かせて結論を後に置く、といった思考順序の制御をキー順で表現できるということです。

使えないキーワードとスキーマの上限値

構造化出力はJSON Schemaのサブセットのため、合成系キーワードの一部が非対応で、プロパティ数・ネスト段数・文字列長・enum数に上限が定められています。

非対応と上限は、スキーマ設計を始める前に読んでおくと手戻りが減ります。次の表は公式ドキュメントに記載された制限を、実装での回避策とセットで並べたものです(出典:Structured model outputs)。

制限の対象 公式が示す非対応・上限の内容 回避するにはどうするか
合成系のキーワード allOf、not、dependentRequired、dependentSchemas、if、then、elseは未対応 条件分岐はスキーマではなくアプリ側のロジックへ移し、モデルには判定結果のキーだけを返させる
ルート(最上位)の構造 ルートのオブジェクトはobjectである必要があり、anyOfは使えない 判別可能な共用体は1階層下げ、ルートにitemのようなキーを1つ置いてその値をanyOfにする
オブジェクトの規模 プロパティは合計5000個まで、ネストは10段まで 大きな抽出は用途別のスキーマへ分割し、複数回の呼び出しに分ける
文字列の総量 プロパティ名・定義名・enum値・const値の総文字数は120,000文字を超えられない 長い説明文はdescriptionへ集約せず、system指示側へ移す
enumの規模 全enumプロパティ合計で1000個まで。1つの文字列enumで250個を超える場合、enum値の総文字数は15,000文字まで マスタ相当の大きな選択肢はenumにせず、IDの文字列として受け取り後段で照合する

ルート直下にanyOfを置けない制限は、TypeScript側でZodを使っていると踏みやすい落とし穴です。公式も「Zodで判別可能な共用体を使うとトップレベルにanyOfが生成されるため、そのままでは動かない」と例示しています。仮の例で言えば、成功レスポンスと失敗レスポンスをz.discriminatedUnionで束ねてそのまま渡すと弾かれるので、z.object({ result: z.discriminatedUnion(...) })のように1段ラップして回避します。

また公式は「strict: trueを指定して未対応のJSON Schemaでリクエストすると、エラーが返る」と明記しています。エラーは黙って無視されるのではなく、リクエスト時点で失敗します。開発初期にスキーマを1回POSTして通ることを確かめておけば、本番のバッチ実行中に初めて気づく事態を避けられます。

$defsと再帰スキーマで同じ定義の重複をなくす

構造化出力は定義($defs)による部分スキーマの再利用と、$refを使った再帰的なスキーマの両方に対応しています。

同じオブジェクト構造が複数箇所に出てくるとき、定義を1か所にまとめられます。公式ドキュメントは「定義はサポートされている。スキーマ全体で参照される部分スキーマを定義するのに使える」とし、さらに「再帰的なスキーマもサポートされている」として、ルート再帰を示す#を使う書き方と、明示的に再帰する書き方の両方を例示しています(Supported schemas)。

仮の例として、手順配列の各要素を$defsに切り出した完成形を示します。要素の定義が1か所に集まるため、フィールドを増やすときの修正箇所も1か所で済みます。

{
  "type": "object",
  "properties": {
    "steps": { "type": "array", "items": { "$ref": "#/$defs/step" } },
    "final_answer": { "type": "string" }
  },
  "$defs": {
    "step": {
      "type": "object",
      "properties": {
        "explanation": { "type": "string" },
        "output": { "type": "string" }
      },
      "required": ["explanation", "output"],
      "additionalProperties": false
    }
  },
  "required": ["steps", "final_answer"],
  "additionalProperties": false
}

再帰が必要になるのは、階層の深さが事前に決まらないデータです。組織図、コメントツリー、入れ子のUI要素などが該当します。公式の連結リストの例では、次の要素を{ "anyOf": [ { "$ref": "#/$defs/linked_list_node" }, { "type": "null" } ] }と書いて、末尾をnullで終端しています。ここで先ほどのネスト10段の上限が効いてくるため、深さが読めない再帰構造では、モデルに最大深さを指示して打ち切らせる設計を併用します。

読者がここで決めるのは「スキーマを平坦に保つか、$defsで構造化するか」です。判断の目安は再利用の回数で、同じ形のオブジェクトが2か所以上に現れた時点で$defsへ切り出すと、後からキーを追加したときの取りこぼしが起きにくくなります。

refusalと打ち切りを検知して後段処理を止める

構造化出力でもスキーマに一致しない応答が返る場合があり、公式は安全上の理由による拒否(refusal)と、最大トークン数に達して応答が不完全になるケースを挙げています。

公式ドキュメントは「モデルが、渡されたJSONスキーマに一致する有効な応答を生成しない場合がある。これはrefusalの場合、つまりモデルが安全上の理由で回答を拒否した場合や、たとえば最大トークン制限に達して応答が不完全な場合に起こりうる」と説明しています。さらにrefusalについては「拒否は必ずしも指定したスキーマに従わないため、APIレスポンスにはモデルが要求の実行を拒否したことを示すrefusalという新しいフィールドが含まれる」としています(Structured model outputs)。

実装は2段階の判定になります。まずresponse.statusがincompleteでincomplete_details.reasonがmax_output_tokensかを見て打ち切りを検知し、次に出力メッセージの中身のtypeがrefusalかoutput_textかで分岐します。公式のサンプルと同じ構造で書くと、次の形です。

if (
  response.status === "incomplete" &&
  response.incomplete_details.reason === "max_output_tokens"
) {
  throw new Error("Incomplete response");
}

const message = response.output.find((item) => item.type === "message");
const content = message?.content[0];

if (content.type === "refusal") {
  console.log(content.refusal);
} else if (content.type === "output_text") {
  console.log(content.text);
}

拒否されたときのレスポンスは、本文の代わりにrefusalを持つ形で返ります。仮の例として受け取る形は次のようになります。

{
  "content": [
    {
      "type": "refusal",
      "refusal": "I am sorry, I cannot assist with that request."
    }
  ]
}

もう1つ、公式が警告する挙動があります。「モデルは常に提供されたスキーマに従おうとするため、入力がスキーマとまったく無関係な場合はハルシネーションにつながることがある」という点です。対策として公式は、有効な応答を作れない入力が来たときの扱いをプロンプトに書くこと、たとえば空のパラメータや特定の一文を返すよう指定することを勧めています。仮の例なら、summaryに「判定不能」と入れさせるルールをsystem指示に置き、後段はその値を人の確認キューへ送ります。

読者がここで決めるのは「失敗をどこで止めるか」です。パースは成功しているのに中身が無意味、という状態は自動処理では検知しにくいため、判定不能の値を明示的に返させて人の目に戻す導線を作るのが現実的です。ChatGPTを仕事で活用して業務効率化するにはどうすればいい?で触れている「自動化と確認の線引き」の考え方が、この分岐の設計にそのまま当てはまります。

ストリーミングとSDKヘルパーでスキーマの二重管理を防ぐ

OpenAIのPythonとJavaScriptのライブラリは、pydantic.BaseModelやz.objectで定義した型からスキーマを生成でき、ストリーミング中の構造化データのパースにも対応しています。

公式ドキュメントは「REST APIでのJSON Schema対応に加えて、PythonとJavaScriptのOpenAIライブラリではpydantic.BaseModelとz.objectを使ってオブジェクトのスキーマを定義できる」と説明しています。RubyのSDKについても、SorbetのT::Structで定義したスキーマに対応し、型付きのパース結果を返すと書かれています(Structured model outputs の Examples)。

これが効くのは、型定義とJSON Schemaの乖離を防げる点です。公式は「JSON Schemaとプログラミング言語側の対応する型が乖離するのを防ぐため、利用できる場合はSDKのネイティブなスキーマヘルパーを使うことを強く推奨する」とし、JSON Schemaを直接書くなら「どちらかが編集されたときにフラグを立てるCIルールを追加するか、型定義からJSON Schemaを自動生成する(またはその逆の)CIステップを追加するとよい」と勧めています。

仮の例として、Pythonでpydanticのモデルからそのまま構造化出力を受け取る完成形を示します。responses.parseにtext_formatとしてクラスを渡すと、output_parsedに型付きの結果が入ります。

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()


class CalendarEvent(BaseModel):
    name: str
    date: str
    participants: list[str]


response = client.responses.parse(
    model="gpt-6-astra",
    input=[
        {"role": "system", "content": "Extract the event information."},
        {"role": "user", "content": "Alice and Bob are going to a science fair on Friday."},
    ],
    text_format=CalendarEvent,
)

event = response.output_parsed

ストリーミングについて公式は「モデルの応答や関数呼び出しの引数を生成中に処理し、構造化データとしてパースできる。JSONのフィールドを1つずつ表示したい場合や、関数呼び出しの引数が使えるようになった時点で処理したい場合にとくに有用」と述べ、「構造化出力でのストリーミングはSDKに任せることを推奨する」としています(Structured model outputs の Streaming)。ストリーミング時はresponse.refusal.deltaとresponse.output_text.deltaが別イベントとして流れるため、拒否の検知も逐次できます。

運用上の注意として、モデルを入れ替えるときはスキーマの再検証をセットにしてください。モデルの提供終了や置き換えの動きはOpenAI モデル非推奨化戦略に整理があります。実装をどこで回すか(CLI・IDE・クラウド)を決めかねている場合はOpenAI Codexの提供形態の違いが判断材料になります。

よくある質問(FAQ)

Q1. 構造化出力を使えば、出力の内容そのものも正しくなりますか。
形式と内容は別の問題です。公式ドキュメントは「構造化出力でも誤りが含まれることはある」と明記し、誤りが見られる場合は指示を調整する、system指示に例を入れる、タスクをより単純なサブタスクに分割する、といった対処を挙げています。保証されるのはスキーマへの準拠であり、事実としての正しさではありません。
Q2. すべてのフィールドをrequiredにすると、任意項目が表現できません。どうしますか。
型の選択肢にnullを加えます。公式は、全フィールドがrequiredであってもユニオン型でnullを使えば任意パラメータを模倣できると説明しています。書き方は"type": ["string", "null"]のように配列で指定し、requiredにはそのキーを含めたままにします。受け取る側は値がnullのケースを未入力として扱います。
Q3. Chat Completionsでも構造化出力は使えますか。
使えます。公式は、構造化出力とJSONモードの両方がResponses API、Chat Completions API、Assistants API、Fine-tuning API、Batch APIでサポートされていると記載しています。2026年10月2日に確認した現行モデル(gpt-6-astraなど)のページでも、Chat Completionsは「Supported」でした。ただしresponse_format: {type: "json_schema", ...}による構造化出力には、公式が挙げるモデル条件があります。古い世代のスナップショットは対象外になる場合があるため、使うモデルIDのページでSupported featuresを確認してください。
Q4. strictを書かなかった場合はどう動きますか。
APIによって既定が異なります。公式のfunction callingガイドによると、Responsesは可能な範囲でスキーマを厳密モードへ正規化しようとし、互換にできない場合は非厳密なベストエフォートの関数呼び出しへフォールバックします。そのときレスポンスのツールにはstrict: falseが表示されます。Chat Completionsは既定で非厳密のままです。意図した挙動にしたいなら明示的に指定してください。
Q5. 同じスキーマを何度も使うと遅くなりますか。
逆です。公式は、あるスキーマでの最初のリクエストはAPIがスキーマを処理する分だけ追加のレイテンシが発生するが、同じスキーマでの後続リクエストには追加のレイテンシは発生しないと注記しています。スキーマをリクエストごとに動的生成するより、固定したスキーマを使い回す方が有利です。
Q6. スキーマのキーの順番に意味はありますか。
あります。公式は、構造化出力を使うと出力はスキーマのキーの順序と同じ順序で生成されると記載しています。根拠や推論の過程を先に、結論を後に置きたい場合は、スキーマ上でもその順にキーを並べます。表示順の都合だけでキーを並べ替えると、モデルが考える順序も変わる点に注意してください。

まとめ|構造化出力を後段処理の前提にするために

構造化出力は、JSONのパース失敗を「例外処理で拾う問題」から「そもそも起こさない設計」へ移す機能です。実装の骨組みは、受け取る側のデータ構造からJSON Schemaを起こし、text.formatにjson_schemaとstrict: trueを渡し、refusalと打ち切りだけを分岐させる、という順番になります。ここまで固めれば、後段のコードはキーの存在チェックから解放されます。

導入時に確認する点をもう一度並べます。使うモデルのSupported featuresにstructured_outputsがあること、スキーマの全オブジェクトにadditionalProperties: falseを書いていること、全フィールドがrequiredで任意項目はnullのユニオンになっていること、allOfなどの非対応キーワードを使っていないこと、ネスト10段とプロパティ5000個の上限に収まっていること。この5点は、リクエストがエラーで弾かれる原因の大半を占めます。

コードを書かずに同じ発想を業務へ持ち込みたい場合は、指示と出力形式をあらかじめ固定しておく方法もあります。カスタムGPT(GPTs)の作り方で、出力フォーマットを指示として固定する設定手順を確認できます。APIとノーコードのどちらで回すかは、処理件数と後段の自動化度合いで決めてください。

このサイトは株式会社S.Lineが運営しています。記事内の店舗予約・LINE登録・講座の案内には、当社または当社が集客を担当するサービスの広告(PR)が含まれます。
本記事は公式情報・専門メディアを参考に執筆しています。最新の正確な情報は引用元の公式ページをご確認ください。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

株式会社S.Line 代表取締役。偏差値39から起業し、AIとSNSの活用で「普通の人」が成果を出す方法を発信。

目次