AI活用

Structured OutputsでJSONを安定生成する方法|スキーマ検証とエラー対処

Structured OutputsでJSONを安定生成する方法は、JSON Schemaを後続処理との契約として固定し、形式保証の後に自社の業務値を検証することです。 この記事では、Node.js、TypeScript、Zod、Responses APIを使い、拒否・未完了・認証エラーを正常データから分離して段階公開する手順を示します。

Structured Outputs実装の前提:スキーマを業務契約として固定する

Structured Outputsを使う目的は、JSONとして読める文字列を得ることではなく、後続サービスが受け取る契約を機械的に固定することです。

OpenAIの公式ガイドは、指定したJSON Schemaへの準拠、拒否の明示、PythonとJavaScript SDKによる型定義支援を説明しています。一方で、対応するのはJSON Schemaの一部であり、全フィールドをrequiredにする必要があります。任意項目はnullとの共用型で表す設計が前提です[1]

この記事の想定環境は、Node.js 20以上、TypeScript 5以上、OpenAI JavaScript SDK、Zod、テストランナー、ステージング用のOpenAI Projectです。API呼び出しはブラウザから直接行わず、自社バックエンドだけに置きます。

OpenAI APIはBearer認証を受け付け、APIキーまたはワークロードID連携で得た短期アクセストークンを使用できます。秘密情報はサーバーの環境変数か鍵管理サービスから読み込み、画面コード、配布アプリ、リポジトリ、ログへ出しません[2]

決める項目本記事の例確認方法
利用場面問い合わせ文から担当部署、緊急度、要約を抽出する後続の振り分け処理が使う項目だけを列挙する
実行境界分類結果は保存するが、顧客への返信は自動送信しない副作用を持つ操作がスキーマ外であることをレビューする
モデル設定OPENAI_MODELでスナップショットを指定するステージングと本番の値を構成管理で比較する
認証ステージング専用Projectと最小権限の資格情報を使う401時に別Projectへ自動フォールバックしない
試験集合通常24件、境界12件、拒否・欠損・長文12件の計48件入力、期待値、判定理由を版管理する

この方式が向かないケース

出力項目を事前に定義できない探索的な対話、自由記述そのものが成果物となる文章制作、誤分類時に人の承認なしで送金・削除・契約変更へ進む処理には適しません。特に副作用の大きい処理では、構造が正しいことと判断が正しいことを混同せず、Structured Outputsの外側に権限確認と承認待ち状態を設けます。

JSONを業務処理へ渡せる完成形

完成形では、入力受付、モデル呼び出し、構造検証、業務値検証、保存、利用の六つを分けます。Structured Outputsが担うのは主に構造検証までです。たとえばpriorityが定義済みの列挙値で返っても、契約上の最優先顧客を正しく判定したとは限りません。文字列の型、必須キー、列挙値はスキーマで保証し、顧客IDの存在、担当部署の稼働状況、受付時刻とSLAの整合は自社コードで検証します。

保存前にはschema_version、使用モデル、プロンプト版、社内トレースID、OpenAIのx-request-id、判定結果を一組にします。

公式API概要はREST APIの現行版をv1、レスポンスヘッダーのopenai-versionを現時点で2020-10-01と示し、本番障害の調査用にリクエストIDの記録を推奨しています[2]。入力本文を丸ごとログへ残す必要はなく、個人情報を除いた入力ハッシュとケースIDで追跡できます。

構造化出力の合格条件は「JSONをパースできた」ではありません。「契約した形で返り、業務ルールにも合格し、拒否や未完了を成功データとして保存していない」状態です。

検証層検証する内容不合格時の扱い
API応答HTTP状態、レスポンス状態、拒否、未完了理由保存せず障害区分へ送る
スキーマ必須キー、型、列挙値、余分なキーの禁止契約違反として開発担当へ通知する
業務値部署コードの存在、要約の長さ、緊急度の根拠人による確認待ちへ移す
副作用保存先、更新権限、自動送信の禁止承認者が許可するまで実行しない

ZodとResponses APIによる実装手順

手順1:読み手ではなく利用側から項目を決める

問い合わせ振り分けで必要なのは、見栄えのよい説明文ではなく、departmentprioritysummaryneeds_human_reviewです。各項目について、利用する画面・処理、許容値、最大長、欠損時の動作を表にします。説明にしか使わない項目を増やすとトークンと変更範囲が膨らむため、初版は一つの後続処理が所有できる大きさに抑えます。

手順2:型定義とAPI用スキーマを同じ場所から生成する

ZodなどSDKが対応する型定義を使えば、TypeScript型とJSON Schemaを別々に手書きするずれを減らせます。公式ガイドはキー名と説明を明確にし、評価を使って構造を決めるよう案内しています[1]。次の例では、任意に見えるレビュー理由もstring | nullとして必須キーにしています。

import OpenAI from "openai";
import { z } from "zod";
import { zodTextFormat } from "openai/helpers/zod";
const Ticket = z.object({
schema_version: z.literal("ticket-v1"),
department: z.enum(["sales", "support", "billing", "unknown"]),
priority: z.enum(["normal", "urgent"]),
summary: z.string().min(1).max(240),
needs_human_review: z.boolean(),
review_reason: z.string().max(160).nullable()
});
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const response = await client.responses.parse({
model: process.env.OPENAI_MODEL,
input: [
{ role: "system", content: "Classify the ticket. Do not invent customer facts." },
{ role: "user", content: sanitizedTicketText }
],
text: { format: zodTextFormat(Ticket, "support_ticket") }
});
const ticket = response.output_parsed;
if (!ticket) throw new Error("NO_PARSED_TICKET");

手順3:構造の後に業務値を検査する

departmentが列挙値でも、その部署が当日受付を停止している可能性があります。社内マスターへ照会し、urgentなら根拠語と顧客契約を確認し、unknownまたはneeds_human_review=trueなら自動振り分けを止めます。ここで落ちたデータはモデルへ無制限に再送せず、原因コードを付けて人のキューへ移します。

手順4:スキーマ版を保存し、互換性を試験する

項目追加時はticket-v2を作り、旧利用側が新データを読めるか、新利用側が保存済みv1を読めるかを別々に試します。列挙値の名称変更は破壊的変更として扱い、単なるプロンプト修正で済ませません。新旧の読み取りを同時に提供できない場合は、保存データの移行手順と旧版へ戻す期限をリリース票に記入します。

手順5:認証とモデルをデプロイ設定へ分離する

OPENAI_API_KEY、Project、モデルスナップショット、タイムアウトはコードへ埋め込まず、環境別設定で切り替えます。401や403を受けたときに別のキーを順番に試す実装は、権限境界と監査証跡を壊します。資格情報エラーは直ちに停止し、鍵の所有者がProject、許可IP、失効状態を確認してから再開します。

スキーマ・拒否・認証・通信の障害例

Structured Outputsでも、すべてのAPI応答が業務データになるわけではありません。安全上の拒否は指定スキーマに従うとは限らず、拒否専用の出力として扱います。また、最大出力トークンへ達した未完了応答も成功データではありません。公式ガイドが挙げる拒否と出力上限の例外を、通常結果とは別の状態へ分岐させます[1]

観測した状態典型原因再試行運用上の処置
400とスキーマエラー未対応キーワード、必須指定漏れ、余分なプロパティ許可同じ要求では行わない直前のスキーマ版へ戻し、契約テストを修正する
401または403失効キー、Project違い、権限不足、許可IP外自動では行わない資格情報を停止し、認証担当へ引き継ぐ
拒否出力安全上応答できない入力文言だけ変えて繰り返さない拒否理由を画面へ示し、必要なら人が確認する
未完了・出力上限説明や配列が長く、上限内に完結しない入力を縮めた一回だけ要約単位を分割し、欠けたJSONを保存しない
429要求数、トークン量、Project上限、利用額上限原因コードを見て判断する流量制御へ送り、上限超過なら停止する
500または503一時的なサービス障害や過負荷ジッター付き待機で上限二回失敗を記録し、旧経路へ切り替える

OpenAIのエラーコード資料では、429にレート制限、クレジット不足、組織・Projectの利用額上限など複数の原因があり、401にもキー、組織、Project、許可IPの違いがあります[3]

HTTP番号だけで再試行可否を決めず、error.code、応答ヘッダー、社内トレースIDを同時に記録します。送信済みデータの保存が完了したか不明な通信切断では、同じ入力を即時再送せず、処理IDの存在を先に照会します。

契約テストと業務値テスト

試験は「パース成功」「業務判断の正解」「例外の安全な停止」を分けます。48件の固定集合に対して、まずスキーマ準拠率を100%にし、その後で部署分類と緊急度を採点します。

評価ケースは本番ログの無作為抽出ではなく、個人情報を匿名化し、正解と理由を複数担当で確定したものを使います。OpenAIのEvals公式資料を、固定データセットと評価処理を継続実行するための一次情報として参照します[4]

合格条件:
1. 48件すべてがスキーマを満たす
2. 拒否・未完了の4件が業務テーブルへ保存されない
3. 部署分類の正解率が45 / 48以上
4. urgentの見逃しが0件
5. v1利用側がv2移行期間中も読み取り可能
6. 401を注入した試験で資格情報の自動切替が起きない
評価軸配点満点条件即時不合格
契約準拠30全件で型、必須、列挙、版が一致一件でも壊れたJSONを保存
業務値30部署45件以上正解、緊急見逃しなし契約顧客の緊急案件を通常扱い
例外処理20拒否、未完了、401、429を別状態で記録拒否文を要約として登録
追跡性10版、モデル、トレースID、判定理由を復元可能入力元を特定できない
費用・性能10予算とP95の両方が基準内上限超過を検知できない

公開候補は90点以上かつ即時不合格ゼロとします。この点数は想定の社内判定であり、OpenAIが定める品質基準ではありません。モデル、プロンプト、スキーマ、SDKのいずれかを変更したら同じ48件を再実行し、前版との差分を保存します。平均点が維持されても緊急案件一件の見逃しが増えた場合は、改善ではなく回帰として扱います。

影響を限定した段階リリース

最初の本番投入では、対象を社内問い合わせの5%または一日50件の小さい方に制限し、Structured Outputsの結果を保存しても自動振り分けには使わないシャドー運転を三営業日行います。業務担当が従来結果と比較し、契約準拠、部署分類、緊急見逃し、P95応答時間、採用一件当たり費用を日次で承認します。次に25%へ広げる際も、自動化するのは担当候補の表示までとし、チケット移動は人が確定します。

公開設定ではstructured_ticket_v1という機能フラグを用意し、モデル、スキーマ版、利用率を別々に変更できるようにします。新経路が停止しても、入力を失わず従来の手動キューへ送れることが必須です。データベースは新しいJSONだけを正本にせず、移行期間中は原文、従来分類、新分類を関連付けます。これにより、切り戻した後も未処理案件と誤分類案件を再構成できます。

変更に耐えるスキーマ設計票

スキーマレビューでは、項目名の好みではなく、利用先、変更頻度、誤りの影響、値の出所を確認します。部署コードのように社内マスターが正本となる値は、モデルの自由記述にしません。要約のように生成が必要な値には最大長と禁止内容を付けます。根拠が入力にない場合は、空文字で埋めずneeds_human_reviewを立てる設計にします。

項目型と制約値の正本変更時の注意
schema_version固定文字列ticket-v1アプリのリリース設定上書きせず新しい版を追加する
department4値の列挙部署コード表名称変更は利用側の移行を伴う
prioritynormalまたはurgentSLA規程閾値変更時は過去の緊急例を再採点する
summary1〜240文字問い合わせ原文個人番号や決済情報を含めない
review_reason160文字以下またはnull判定不能の条件false時はnullという相互条件をコードで検査する

JSON Schemaだけでは「needs_human_review=falseならreview_reason=null」のような複数項目間ルールや、存在する部署だけを許す照合を十分に表せない場合があります。その制約を無理にプロンプトへ詰めず、アプリケーション側の関数として名前を付けます。仕様書にはスキーマ保証と業務保証の担当を分けて記載し、テスト失敗時の修正先を明確にします。

本番で原因を追える処理記録と監視

監視単位は一回のAPI呼び出しではなく、一件の問い合わせ分類です。同じ案件で再試行が起きても、社内のtrace_idは維持し、API呼び出しごとにattemptを増やします。

記録するのは受付時刻、匿名化ケースID、スキーマ版、モデル、HTTP状態、レスポンス状態、拒否・未完了区分、入力・出力トークン、処理時間、業務検証結果、OpenAIのリクエストIDです。原文とAPIキーは監視ログへ入れません。

ダッシュボードでは、契約違反率、業務値不合格率、拒否率、P50・P95応答時間、入力・出力トークン、再試行回数を別系列にします。構造準拠が100%でも業務値不合格が増えれば、スキーマではなく指示や分類基準を調べます。401が一件でも出たら鍵関連の警告、429が5分間に3件以上なら流量警告、P95が基準の1.5倍を15分超えたら性能警告を出す、というように観測値と担当を対にします。

障害票には「APIが悪い」「AIが不安定」と書かず、最初に期待と異なった層を残します。たとえば「HTTP 200、応答completed、スキーマ合格、部署コード不合格、入力ケースT-031、ticket-v1」のように記録すれば、API障害ではなく社内マスターとの不整合だと切り分けられます。アラートから同じ固定ケースを再実行できるリンクを用意すると、復旧確認の属人化も抑えられます。

費用・性能・品質を判定する評価票

性能は平均だけでなく、利用者が待つ長い側を示すP95で見ます。費用はAPI請求だけでなく、人の確認を含む採用一件当たり費用へ変換します。

次の数値は実績ではなく、計算方法を示す想定例です。月10,000件、API費用48,000円、人による例外確認が300件、1件4分、労務単価3,000円/時なら、確認費は300×4÷60×3,000=60,000円です。採用が9,500件なら、採用一件当たりは(48,000+60,000)÷9,500=約11.4円となります。

遅延測定では、デプロイ直後の一回と定常時を混ぜません。公式ガイドは、新しいスキーマを初めて使う要求ではスキーマ処理による追加遅延が生じ、その後の同一スキーマ要求には同じ追加遅延がない場合を説明しています[1]

リリース手順に匿名のウォームアップ要求を一回含め、初回時間、定常P50、定常P95を別欄へ記録します。新しいスキーマ版へ切り替えるたびに初回値を取り直し、ウォームアップ失敗を利用者の本番要求で肩代わりさせません。

API費用は、入力トークン、出力トークン、再試行分を呼び出し単位で集計し、月末の請求額と照合します。人の確認費には、例外を読む時間だけでなく、誤分類を戻す時間と障害調査時間も含めます。ただし初期開発費は運用単価へ一括で混ぜず、償却期間を定めた投資回収の表で別に扱います。母数が少ない試行期間は一件の障害で率が大きく動くため、件数と率を必ず並記します。

指標想定の合格線単独では分からないこと
契約準拠率スキーマ合格件数÷完了応答数100%部署分類の正しさ
業務採用率人が修正せず採用した件数÷全対象95%以上重大な見逃しの有無
P95応答時間処理時間を昇順に並べた95%位置3.5秒以下費用と回答品質
採用一件当たり費用API費用と確認費の合計÷採用件数15円以下削減した工数の使途
例外滞留24時間を超えた人確認待ち件数0件通常案件の速度

合格線はこの問い合わせ分類の想定値であり、別業務へそのまま移しません。性能を上げるため出力長を削った結果、緊急理由が不足することもあります。週次判定では、品質、速度、費用の三つを共通の母数と期間で比較し、モデルだけでなく入力長、出力長、例外確認時間の変化も記録します。

境界・障害を含む48件の試験ケース

通常文だけで構成準拠を確認しても、本番で困る入力は残ります。固定48件には、複数部署にまたがる相談、本文なし、非常に長い引用、部署名の表記揺れ、緊急語を含む通常案件、緊急語を含まない契約上の緊急案件、拒否を誘う入力、出力上限、401、429、500、タイムアウトを含めます。障害応答はテスト用のアダプターで注入し、実サービスへ負荷を掛けて再現しません。

ケース入力または注入状態期待結果
部署競合請求取消と製品不具合を同時に相談unknownと人確認を返し、自動移動しない
根拠不足「至急お願いします」だけを入力要約は作るが緊急判定を確定しない
拒否安全上処理できない内容拒否状態を記録し、ticket表へ挿入しない
出力上限低い上限をテスト設定で指定未完了として止まり、断片JSONを破棄する
認証失敗失効したステージング資格情報一回で停止し、別キーを探索しない
版不一致v1利用側へv2だけの列挙値を渡す契約テストがデプロイ前に失敗する

各ケースには期待JSONだけでなく、保存可否、利用者へ見せる状態、担当者、再試行回数も持たせます。これにより「正しい値を返したが、自動送信してはいけない」という業務上の失敗も検出できます。新しい本番障害が見つかったら、個人情報を除いた最小再現ケースを追加し、既存ケースを置き換えず履歴を残します。

停止条件と旧経路への切り戻し

停止条件は本番投入前に数値と即時条件へ分けます。この問い合わせ分類では、壊れたJSONの保存、緊急案件の見逃し、資格情報の露出、誤った自動送信を一件でも確認した時点で機能フラグを0%にします。契約準拠率が99.9%未満、P95が5秒超、採用一件当たり費用が25円超、429が全要求の2%超のいずれかが二つの連続した15分窓で発生した場合も、新経路を停止します。

  1. 機能フラグを無効にし、新規案件を従来の手動キューへ送る。
  2. 処理中の案件をpending_reviewへ移し、自動再試行を止める。
  3. 最後に承認済みのモデル、プロンプト、ticket-v1へ設定を戻す。
  4. 障害時間帯の案件IDを抽出し、保存・移動・通知の有無を照合する。
  5. 48件の固定試験と障害再現ケースが通るまで利用率を上げない。

データ漏えいの疑い、権限外Projectへの記録、顧客通知の誤送信は、アプリ担当だけで再開を決めません。情報セキュリティ、法務または個人情報保護担当、業務責任者へ引き継ぎ、影響範囲と通知要否を確定します。スキーマ変更が原因でも、保存済みデータを旧版へ戻せない場合はロールバックではなくデータ移行障害として扱い、復旧計画を別に立てます。

最小リリース判定を行う

次の行動は、実装を広げることではなく、問い合わせ分類の契約を一枚に確定することです。ticket-v1の項目、業務値検証、48件のケース、90点の受入評価票、停止条件、従来キューへの切り戻し責任者を一回のレビューで承認します。

その後、ステージングProjectでコード例を動かし、スキーマ準拠100%、緊急見逃しゼロ、401時の停止、機能フラグの切替を証跡付きで確認します。

この判定を満たすまでは本番データを送信せず、満たした後も5%のシャドー運転から始めます。Structured Outputsの導入効果はJSONの見た目ではなく、誤った振り分けを増やさず、人が修正せず使える分類を予算と応答時間の範囲内で返せたかによって判断します。

参考文献・出典

  1. OpenAI公式ドキュメント「Structured model outputs」(Structured Outputs実装で使えるJSON Schemaの範囲、必須項目、拒否、初回遅延を説明。仕様版:版表記なし(2026-07-30確認))
  2. OpenAI公式APIリファレンス「API Overview」(構造化出力APIの認証、REST API v1、openai-version 2020-10-01、リクエストIDを説明。仕様版:版表記なし(2026-07-30確認))
  3. OpenAI公式ドキュメント「Error codes」(構造化出力APIで起こる401・429などの原因切り分けに使用。仕様版:版表記なし(2026-07-30確認))
  4. OpenAI公式ドキュメント「Evals」(Structured Outputsのスキーマ準拠と業務値を固定データセットで評価する方法に使用。仕様版:版表記なし(2026-07-30確認))

関連記事

新着記事
  1. AIの回答をストリーミング表示する方法|UXとエラー処理の設計

  2. Structured OutputsでJSONを安定生成する方法|スキーマ検証とエラー対処

  3. Tool Callingで業務を自動化する方法|分岐・承認・再試行の設計

TOP

EmMatch AIPをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む