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つのフィールドが必要です。

  • categorybillingtechnicalaccountのいずれか。
  • 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
}

すべてのプロパティを必須にし、additionalPropertiesfalseにすると、後続処理で扱いやすい安定したオブジェクトになります。ただし、すべての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を参照してください。

どちらのエンドポイントを選んでも、次の方針を維持します。

  1. JSON Schemaのバージョン付き情報源を1つにする。
  2. 意味を変えず、エンドポイントごとのラッパーだけを変換する。
  3. 完了したレスポンスを利用前に検証する。
  4. 構造検証の後で決定的なビジネスルールを適用する。
  5. 解析、拒否、切り詰め、互換性の失敗を別々に監視する。

クライアントの初期移行はOpenAI互換APIガイドから始めます。イベント解析や未完了のStreamについてはAI APIストリーミングガイドを参照してください。モデルと料金で現在のモデルとグループを確認した後、API Keyが実際に使用するルートでStructured Outputsを検証します。

Structured Outputsの価値は、モデルレスポンスを信頼できるアプリケーションインターフェースへ絞り込める点にあります。ただし、事実確認、認可、課金ロジックの代わりにはなりません。「OpenAI互換」もBase URLから推測できる保証ではなく、機能ごとに検証すべき契約です。