OpenAI互換APIのStructured Outputs:JSON Schemaガイド
ResponsesとChat Completionsで厳格なJSON Schemaを使う方法をフィールド単位で説明し、アプリ検証、拒否と切断、ルート互換性テストを扱います。
Structured Outputsを使うと、単に「JSONで返して」とモデルへ指示するのではなく、指定したスキーマに従うJSONを要求できます。OpenAI互換APIでは同じスキーマをResponsesとChat Completionsのどちらでも利用できますが、リクエストフィールドは異なります。また、選択したモデルとルートで実際に対応しているかを検証する必要があります。
本番環境では3つのレイヤーで安全性を確保します。モデルが対応している場合はstrictなスキーマ出力を使い、アプリケーションでも返されたJSONを解析・検証し、その後で値に対して決定的なビジネスルールを適用します。スキーマ準拠によって多くの形式エラーを排除できますが、モデルの回答が事実的・意味的に正しいことまでは証明できません。
Structured OutputsとJSON Modeは同じではない
モデルへJSON形式の出力を求める方法は、主に次の3つです。
| 方法 | 有効なJSONを生成 | 指定スキーマを強制 | 主な用途 |
|---|---|---|---|
| Promptだけで指示 | 保証されない | いいえ | 解析失敗を許容できるプロトタイプ |
| JSON Mode | 対応し、正常に完了した場合は可能 | いいえ | 形をアプリケーション側で検証する柔軟なJSON |
strict: trueのStructured Outputs |
完了状態と拒否を適切に処理する場合は可能 | 対応するJSON Schemaサブセット内で可能 | 型付きデータ抽出とアプリケーションワークフロー |
OpenAIのStructured Outputsガイドでは、選択したモデルとエンドポイントが対応している場合、JSON ModeよりStructured Outputsを優先することを推奨しています。同ガイドは2つの異なる用途も区別しています。
- モデルからユーザーへ予測可能なデータオブジェクトを返す場合は、構造化レスポンス形式を使う。
- モデルからアプリケーションへ処理の実行を要求する場合は、Function Callingを使う。
この記事では前者を扱います。ツールループでも厳格な引数スキーマを再利用できますが、Call IDと結果メッセージは別の契約です。
エンドポイントを選ぶ前に1つのスキーマを定義する
サポートチケットの振り分けを例にします。アプリケーションには4つのフィールドが必要です。
category:billing、technical、accountのいずれか。priority:1から3までの整数。requires_human:真偽値。summary:サポートキュー向けの短い説明。
JSON Schemaは次のようになります。
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account"]
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 3
},
"requires_human": {
"type": "boolean"
},
"summary": {
"type": "string"
}
},
"required": ["category", "priority", "requires_human", "summary"],
"additionalProperties": false
}
すべてのプロパティを必須にし、additionalPropertiesをfalseにすると、後続処理で扱いやすい安定したオブジェクトになります。ただし、すべてのJSON Schemaキーワードがプロバイダー間で移植可能になるわけではありません。OpenAIは文書化されたサブセットをサポートしており、別のOpenAI互換プロバイダーは異なるサブセット、または厳格なスキーマモード自体をサポートしない可能性があります。
Responses APIでスキーマを送信する
Responses APIではレスポンススキーマをtext.formatに置きます。Modelflare API Keyを設定し、そのKeyで現在利用でき、Structured Outputsへの対応を確認済みのモデルを選択します。
export MODELFLARE_API_KEY="<YOUR_API_KEY>"
export MODEL_ID="<MODEL_WITH_VERIFIED_STRUCTURED_OUTPUT_SUPPORT>"
curl -sS https://modelflare.dev/v1/responses \
-H "Authorization: Bearer $MODELFLARE_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"$MODEL_ID\",
\"input\": \"The customer was charged twice and wants a refund.\",
\"text\": {
\"format\": {
\"type\": \"json_schema\",
\"name\": \"support_ticket\",
\"schema\": {
\"type\": \"object\",
\"properties\": {
\"category\": {
\"type\": \"string\",
\"enum\": [\"billing\", \"technical\", \"account\"]
},
\"priority\": {
\"type\": \"integer\",
\"minimum\": 1,
\"maximum\": 3
},
\"requires_human\": { \"type\": \"boolean\" },
\"summary\": { \"type\": \"string\" }
},
\"required\": [\"category\", \"priority\", \"requires_human\", \"summary\"],
\"additionalProperties\": false
},
\"strict\": true
}
}
}"
Responsesの生成内容は、型を持つOutput Itemに含まれます。SDKは出力テキストの集約や解析済み出力などの便利なHelperを提供する場合がありますが、通常のHTTPクライアントでは構造化オブジェクトがトップレベルフィールドにあると仮定できません。完了したResponse Objectを読み、そのテキスト出力を見つけてJSONとして解析します。
Chat Completionsで同じスキーマを送信する
Chat Completionsではレスポンススキーマをresponse_format.json_schemaに置きます。スキーマ本体は変わらず、外側のラッパーだけが異なります。
curl -sS https://modelflare.dev/v1/chat/completions \
-H "Authorization: Bearer $MODELFLARE_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"$MODEL_ID\",
\"messages\": [
{
\"role\": \"user\",
\"content\": \"The customer was charged twice and wants a refund.\"
}
],
\"response_format\": {
\"type\": \"json_schema\",
\"json_schema\": {
\"name\": \"support_ticket\",
\"schema\": {
\"type\": \"object\",
\"properties\": {
\"category\": {
\"type\": \"string\",
\"enum\": [\"billing\", \"technical\", \"account\"]
},
\"priority\": {
\"type\": \"integer\",
\"minimum\": 1,
\"maximum\": 3
},
\"requires_human\": { \"type\": \"boolean\" },
\"summary\": { \"type\": \"string\" }
},
\"required\": [\"category\", \"priority\", \"requires_human\", \"summary\"],
\"additionalProperties\": false
},
\"strict\": true
}
}
}"
Chat Completionsでは、JSONテキストは通常choices[0].message.contentに返されます。Contentの文字列を解析し、すでに型付きオブジェクトになっているとは考えないでください。
2つのラッパーは次のように対応します。
| 目的 | Responses API | Chat Completions |
|---|---|---|
| スキーマのコンテナ | text.format |
response_format.json_schema |
| 形式タイプ | text.format.type |
response_format.type |
| スキーマ名 | text.format.name |
response_format.json_schema.name |
| スキーマ本体 | text.format.schema |
response_format.json_schema.schema |
| Strictフラグ | text.format.strict |
response_format.json_schema.strict |
| 結果の場所 | 型付きOutput Item / Output Text Helper | choices[0].message.content |
ModelflareのOpenAI互換レイヤーには、対応するOpenAI/Codexルート上でこの2つのラッパーを明示的に変換する処理があります。ただし、変換によってStructured Outputsに対応していないアップストリームモデルへ新しい能力を追加することはできません。その他のモデルファミリーが元のChat Completionsをそのまま通す場合は、アップストリームプロバイダーの正確なリクエスト契約が情報源になります。
構造と意味を分けて検証する
モデルが次のオブジェクトを返したとします。
{
"category": "billing",
"priority": 2,
"requires_human": true,
"summary": "Customer reports a duplicate charge and requests a refund."
}
このオブジェクトはスキーマに適合しますが、アプリケーションでは2種類の検証が必要です。
構造の検証
JSON Schema ValidatorまたはSDKの型付きHelperで次を確認します。
- すべての必須プロパティが存在する。
- 想定外のプロパティがない。
- 各値が必要な型を持つ。
- Categoryが許可されたEnumに含まれる。
- Priorityが許可範囲内にある。
プロバイダーが厳格な準拠を保証していても、アプリケーション側の検証は必要です。未対応のルート、統合ミス、切り詰められた内容、将来の契約変更から後続システムを保護できます。
意味とビジネスルールの検証
スキーマだけでは、顧客が本当に二重請求されたのか、Priority 2が正しいのか、返金に人間の承認が必要かを判断できません。これらには信頼できる業務データとアプリケーションポリシーが必要です。
影響の大きいワークフローでは、モデルのオブジェクトを分類案として扱います。正規の記録と比較し、権限や金銭的な制限を決定的なコードで適用し、安全な監査参照を残してください。構造的に有効なモデル出力であっても、認可、課金、セキュリティ境界を迂回させてはいけません。
未完了や例外的な出力を処理する
本番用パーサーには、正常系のJSON.parse以外の処理も必要です。
拒否
モデルは安全上の理由でリクエストを拒否する場合があります。OpenAIのStructured Outputs契約では、拒否を通常のスキーマ出力と区別できます。JSONオブジェクトを探す前にResponse Statusと拒否表現を確認し、拒否をスキーマ解析エラーとして報告しないようにします。
切り詰めと出力上限
オブジェクトが完成する前に生成が停止した場合、スキーマから欠けた末尾を復元することはできません。エンドポイントの完了状態とStop Reasonを確認します。最大の有効オブジェクトを収められる出力上限を設定しつつ、スキーマと要求内容自体は有界に保ちます。
未対応のスキーマキーワード
厳格な出力実装は通常、JSON Schemaの一部だけをサポートします。Object、Array、Primitive Type、Enum、Required Field、明示的なAdditional Propertyの扱いから始めます。参照、再帰構造、複雑なUnion、高度な検証キーワードを使う前に、現在のプロバイダードキュメントを確認してください。
スキーマ初回利用時のレイテンシ
プロバイダーによっては新しいスキーマを前処理してキャッシュします。そのため、新しいスキーマの最初のリクエストが、後続リクエストより遅くなる場合があります。リクエストごとに異なるスキーマを生成せず、安定したバージョン付きスキーマを再利用します。レイテンシ比較では初回利用と定常状態も分けます。
モデルまたはルートの非互換
OpenAI互換エンドポイントが通常のChat Completionsを受け付けても、json_schemaを拒否したり、strictを無視したり、未対応モデルへそのまま転送したりする可能性があります。JSONを含む200レスポンスだけでは厳格な準拠の証明になりません。無効値や境界値もテストし、結果オブジェクトを検証してください。
本番前に互換性テストを実行する
有効化する予定のモデルとルートごとに、小さなテストマトリクスを実行します。
| テスト | 記録する証跡 |
|---|---|
| 最小の必須オブジェクト | Status、Model、Route、解析済みオブジェクト、検証結果 |
| すべてのEnum値 | 各許可値を生成・解析できるか |
| 情報不足 | 有界な代替値を使うか、追加情報を求めるか |
| 安全ポリシーを誘発する入力 | 拒否の表現とアプリケーション処理 |
| 小さい出力上限 | 完了状態と切り詰め処理 |
| 未対応のスキーマ機能 | 明示的なエラーか、制約の暗黙的な無視か |
| ResponsesとChatのラッパー | 同等のアプリケーションオブジェクトになるか |
| 安定したスキーマの反復 | 初回利用と定常状態のタイミング差 |
異なるスキーマ、Prompt、リージョン、ストリーミングモード、出力上限でモデルを比較し、差をモデル品質だけに帰属させないでください。プロバイダーの動作やモデルの可用性は変化するため、正確なテスト日も記録します。
ワークフローを検証してからエンドポイントを選ぶ
型付きOutput Item、Responsesのストリーミングイベント、またはより広いResponsesのツールワークフローをすでに使う場合はResponsesが適しています。安定したMessagesベースの統合があり、選択したルートがresponse_format契約をサポートする場合はChat Completionsを利用できます。より一般的な判断基準はResponses APIとChat Completionsを参照してください。
どちらのエンドポイントを選んでも、次の方針を維持します。
- JSON Schemaのバージョン付き情報源を1つにする。
- 意味を変えず、エンドポイントごとのラッパーだけを変換する。
- 完了したレスポンスを利用前に検証する。
- 構造検証の後で決定的なビジネスルールを適用する。
- 解析、拒否、切り詰め、互換性の失敗を別々に監視する。
クライアントの初期移行はOpenAI互換APIガイドから始めます。イベント解析や未完了のStreamについてはAI APIストリーミングガイドを参照してください。モデルと料金で現在のモデルとグループを確認した後、API Keyが実際に使用するルートでStructured Outputsを検証します。
Structured Outputsの価値は、モデルレスポンスを信頼できるアプリケーションインターフェースへ絞り込める点にあります。ただし、事実確認、認可、課金ロジックの代わりにはなりません。「OpenAI互換」もBase URLから推測できる保証ではなく、機能ごとに検証すべき契約です。