DEV Community

Cover image for OpenAI 構造化出力の使い方
Akira
Akira

Posted on • Originally published at apidog.com

OpenAI 構造化出力の使い方

このガイドでは、OpenAIの構造化出力を自分のコードから呼び出し、JSONスキーマに準拠したレスポンスを扱う実装手順をまとめます。モデルにJSONスキーマを渡し、strict: trueを設定すると、要求した形式に一致する応答を得られます。最後に、応答が本当に契約どおりかを検証するために、ApidogでAPIテストコレクションを生成する流れまで確認します。

今すぐApidogを試す

始める前に必要なもの

構造化出力は、モデルの生成結果を指定したJSONスキーマに制約する機能です。strict: trueを設定したスキーマを渡すと、モデルはスキーマ外のフィールドを出力できず、必須キー、型、enum値がスキーマに沿った形になります。

従来の「JSONのみで応答してください」というプロンプトでは、JSONの前後に説明文が混ざったり、期待した型と違う値が返ったりする可能性があります。構造化出力では、プロンプト上のお願いではなく、デコード時の制約として形式を強制できます。

このガイドを進めるには、次を用意してください。

  • OPENAI_API_KEYとして設定したOpenAI APIキー
  • 厳格なスキーマ強制をサポートするモデル
  • 出力形式を定義するJSONスキーマ

適切なモデルを選択する

構造化出力は、OpenAIの最近のモデル、GPT-4oファミリーからGPT-5シリーズに至るまで利用可能です。OpenAIのドキュメントでは、現在、最新の主力モデル(執筆時点ではgpt-5.5)で新しいプロジェクトを開始することを推奨しています。

古いモデルやgpt-3.5時代のモデルはJSONモードをサポートしていますが、厳格なスキーマ強制はサポートしていません。strict: trueに依存する場合は、出荷前に対象のモデルIDが構造化出力をサポートしていることを確認してください。

OpenAIには混同しやすい2つの機能があります。

JSONモード

{
  "response_format": {
    "type": "json_object"
  }
}
Enter fullscreen mode Exit fullscreen mode

JSONモードは、出力が構文的に有効なJSONであることを保証します。ただし、フィールド名、型、必須キー、enumまでは強制しません。アプリケーション側で検証が必要です。

構造化出力

{
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "strict": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

構造化出力は、有効なJSONであることに加えて、指定したJSONスキーマへの準拠を強制します。新規実装では、固定されたレスポンス形式が必要なら構造化出力を使うのが基本です。

JSONモード 構造化出力(厳格)
パラメータ response_format: {"type":"json_object"} type: "json_schema"strict: trueを含むresponse_format
有効なJSON はい はい
スキーマに一致 いいえ はい
必須フィールドの強制 いいえ はい
型とenumの強制 いいえ はい
下流での検証 常に必要 推奨

APIに関する注意点として、Chat Completionsエンドポイントではresponse_formatを使います。一方、新しいResponses APIでは、text.formatの下でtype: "json_schema"を指定します。スキーマのルールは同じですが、フィールドパスが異なるため、利用するエンドポイントの現在のドキュメントを確認してください。

最初のリクエストを送信する

例として、サポートチケットの自由文を型付きレコードに変換します。

次のリクエストでは、summarycategoryseverityaccount_idを持つJSONだけを返すように指定しています。

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.5",
    "messages": [
      { "role": "system", "content": "Extract the ticket into the schema." },
      { "role": "user", "content": "My checkout 500s every time I use a saved card. Started today. 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
        }
      }
    }
  }'
Enter fullscreen mode Exit fullscreen mode

実装時に重要なのは次の3点です。

  • response_format.typejson_schemaを指定する
  • json_schema.stricttrueにする
  • すべてのプロパティをrequiredに含める

応答を読み取る

モデルは、スキーマに一致するJSON文字列をcontentとして返します。

例:

{
  "summary": "Checkout returns HTTP 500 when paying with a saved card",
  "category": "bug",
  "severity": 3,
  "account_id": "acct_8842"
}
Enter fullscreen mode Exit fullscreen mode

account_idは、文字列またはnullを許可するためにanyOfを使っています。

"account_id": {
  "anyOf": [
    { "type": "string" },
    { "type": "null" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

構造化出力では、通常の意味での「任意フィールド」は使えません。キーは常に存在させ、値がない状態をnullで表現します。

スキーマのサブセットに留まる

構造化出力は、JSONスキーマのサブセットを受け入れます。OpenAIが制約を確実に適用し、コンパイル済みスキーマをキャッシュできるようにするためです。

実装時は、次のルールを守ってください。

1. ルートはオブジェクトにする

トップレベルを配列や文字列にはできません。

NG:

{
  "type": "array",
  "items": {
    "type": "string"
  }
}
Enter fullscreen mode Exit fullscreen mode

OK:

{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": ["items"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

2. すべてのプロパティをrequiredに含める

構造化出力では、定義したプロパティをすべてrequiredに入れます。

{
  "type": "object",
  "properties": {
    "email": {
      "anyOf": [
        { "type": "string" },
        { "type": "null" }
      ]
    }
  },
  "required": ["email"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

「値がないかもしれない」フィールドは、nullを許可して表現します。

3. additionalPropertiesfalseにする

すべてのオブジェクトで、余分なキーを禁止します。

{
  "type": "object",
  "properties": {
    "status": {
      "type": "string",
      "enum": ["open", "closed"]
    }
  },
  "required": ["status"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

4. スキーマを大きくしすぎない

スキーマにはサイズ制限があります。目安として、最大約100個のオブジェクトプロパティ、最大5レベルのネストに収めます。深すぎるスキーマや広すぎるスキーマは拒否される可能性があるため、可能な限りフラットにしてください。

5. 検証専用キーワードに依存しない

次のようなキーワードは、モデルによって保証されません。

  • pattern
  • format
  • minLength
  • minimum

たとえばメール形式、日付形式、数値範囲を厳密に保証したい場合は、レスポンス受信後にアプリケーション側で検証してください。

6. 初回呼び出しの遅延を考慮する

新しいスキーマでの最初のリクエストでは、スキーマのコンパイルに時間がかかることがあります。通常は数秒ですが、複雑なスキーマでは最大1分程度かかる場合があります。その後はキャッシュされ、高速になります。

検証キーワードは強制されないため、「保証されたJSON」は「ビジネスルールまで保証された値」ではありません。構造は固定されますが、値の妥当性は別途テストしてください。oneOf/anyOf/allOfでオプションまたはユニオンフィールドをモデル化した経験がある場合、この考え方は近いです。

拒否と切り捨てを処理する

構造化出力でも、レスポンスがスキーマに沿わないケースがあります。

代表的なのは、モデルが安全上の理由でリクエストを拒否した場合です。この場合、スキーマに沿ったcontentではなく、メッセージにrefusalフィールドが返されます。

パース前に必ず分岐してください。

import json

msg = response.choices[0].message

if msg.refusal:
    handle_refusal(msg.refusal)
else:
    ticket = json.loads(msg.content)
Enter fullscreen mode Exit fullscreen mode

拒否はプログラムで検出できるため、謝罪文や定型文をテキスト検索するより安全です。

ほかにも、次のケースではスキーマどおりのJSONを得られない可能性があります。

  • オブジェクト生成中にmax_tokensへ到達し、JSONが切り捨てられる
  • 構造化出力がサポートしない並列ツール呼び出しを使う

ツール呼び出しと組み合わせる場合は、必要に応じて次のように設定します。

{
  "parallel_tool_calls": false
}
Enter fullscreen mode Exit fullscreen mode

Apidogでテストする方法

厳格モードは、生成時にスキーマを強制します。ただし、テストが不要になるわけではありません。

次のような変更で、期待する契約が壊れる可能性があります。

  • モデルIDの変更
  • プロンプトの変更
  • スキーマの変更
  • required配列の編集
  • 拒否パスの変更
  • 下流サービスの期待値の変更

そのため、レスポンスが契約から外れたときに失敗するテストを用意します。ここでApidogを使えます。

Apidog

役割分担は明確です。OpenAIの厳格モードは、スキーマに準拠したJSONを生成します。Apidogはモデル側でスキーマを強制するものではありません。Apidogは、受け取った応答を期待するスキーマに対して検証し、ズレをCIやテスト段階で検出します。

1. Apidogでリクエストを作成する

ApidogでChat Completions呼び出しを作成し、response_formatブロックを含めます。

リクエスト本文には、先ほどのような構造化出力の設定を入れます。

{
  "model": "gpt-5.5",
  "messages": [
    {
      "role": "system",
      "content": "Extract the ticket into the schema."
    },
    {
      "role": "user",
      "content": "My checkout 500s every time I use a saved card. Started today. 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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

繰り返し実行できるように、コレクションへ保存します。

2. レスポンス形式をアサートする

Apidogで応答アサーションを追加します。

確認する項目の例:

  • categorybillingbugaccountotherのいずれか
  • severityが整数
  • account_idが文字列またはnull
  • 余分なキーが存在しない
  • 必須キーがすべて存在する

ApidogはJSONスキーマに対して応答を検証できるため、OpenAIに渡したものと同じスキーマをテストにも使えます。

3. CIで実行する

保存したコレクションをCIパイプラインに組み込みます。

実行タイミングの例:

  • プロンプトを変更したとき
  • モデルIDを変更したとき
  • JSONスキーマを変更したとき
  • 下流の型定義を変更したとき

これにより、静かなスキーマ破損をビルド失敗として検出できます。

4. モックAPIで下流を先に開発する

実際のOpenAI呼び出しがまだない場合や、トークンを消費せずに下流コンシューマをテストしたい場合は、スキーマに準拠したサンプル応答を返すモックAPIを作成します。

これにより、フロントエンドや別サービスは、安定したレスポンス形式に対して先に実装できます。準備ができたら、モックを実際のOpenAI呼び出しに置き換えます。

Apidogをダウンロードすると、リクエスト、アサーション、モックを一箇所で管理できます。

よくある質問

構造化出力があるなら、JSONモードは不要ですか?

JSONモードは引き続き利用できます。有効なJSONを返すことは保証しますが、スキーマは強制しません。

新しいコードで固定形式が必要な場合は、strict: trueを使った構造化出力を選ぶのが基本です。厳格なスキーマをサポートしないモデルを使う場合や、固定形式が不要な場合にJSONモードを検討してください。

スキーマのルートを配列にできますか?

できません。トップレベルはオブジェクトである必要があります。

配列を返したい場合は、次のようにプロパティにラップします。

{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": ["items"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

フィールドをオプションにするにはどうすればよいですか?

構造化出力では、すべてのプロパティをrequiredに含めます。そのため、従来のオプションフィールドはありません。

「欠落する可能性がある」値は、null許容で表現します。

{
  "type": "object",
  "properties": {
    "phone": {
      "anyOf": [
        { "type": "string" },
        { "type": "null" }
      ]
    }
  },
  "required": ["phone"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

キーは常に存在し、値がnullになる可能性があります。

厳格モードなら検証を完全にスキップできますか?

形式面のチェックはかなり減らせます。ただし、patternformat、数値範囲などはモデルによって強制されません。また、拒否や切り捨てによって通常とは異なるレスポンスになる可能性もあります。

そのため、適合性テストは依然として有効です。JSONスキーマに慣れていない場合は、JSONスキーマ入門で基本的な構成要素を確認できます。

どのモデルを使うべきですか?

構造化出力は、GPT-4o以降のモデル、GPT-5シリーズを含むすべてで動作します。OpenAIのドキュメントでは、新しいプロジェクトには現在の主力モデルを使うことを推奨しています。

ただし、サポートはモデルバージョンに紐付きます。strict: trueに依存する前に、使用する正確なモデルIDが厳格モードをサポートしていることを確認してください。

まとめ

構造化出力を使う実装手順は次のとおりです。

  1. 厳格モードをサポートするモデルを選ぶ
  2. json_schemastrict: trueを指定してリクエストを送る
  3. OpenAIがサポートするJSONスキーマのサブセット内で設計する
  4. オプション値はnull許容で表現する
  5. refusalをパース前に分岐処理する
  6. 値レベルのルールはアプリケーション側やテストで検証する
  7. Apidogでリクエスト、アサーション、モックを管理する

Apidogを使えば、OpenAIへのリクエストを保存し、レスポンスをスキーマに対して検証し、統合が安定するまでモックで下流開発を進められます。モデルは形式を約束しますが、テストがそれが維持されていることを証明します。

Top comments (0)