Structured OutputsでJSON Schemaを指定しても、業務上必要な内容がすべて出力されたとは限りません。Schemaは出力の形を制約しますが、生成がtoken上限などで途中終了すれば、レスポンス自体が未完了になり得ます。最初にResponses APIかChat Completionsかを識別し、それぞれの完了状態・終了理由・refusalを確認してください。その後でJSONのparse、schema適合、業務上の必須項目を別々に検証します。どれか一つが失敗した結果は確定データとして保存・送信せず、再生成・分割・人手確認のどれにするか決めます。
目次
「JSONとして読める」と「処理が完了した」は別
Structured Outputsは、モデルの出力を指定schemaへ従わせるための機能です。しかし、schemaに沿った値でも、対象レコードが一部しかない、配列の残りがない、必須の業務判断が省略されていることがあります。逆に、JSONが途中で切れて構文エラーでも、エラーを握りつぶして部分文字列を利用すると、下流で誤った確定処理を起こしかねません。
結果を受け取ったら、次の四段階を独立に確認します。
-
API実行状態:成功した最終状態か。Responsesならstatus、Chat Completionsならchoiceのfinish_reasonを確認する。
-
拒否・中断状態:refusalやcontent filter等、通常の回答とは違う結果がないか確認する。
-
形式:出力が存在し、JSONとしてparseでき、期待したschemaへ適合するか検証する。
-
意味と完全性:業務上必要なID、対象件数、必須フィールド、範囲、参照元がそろい、矛盾や欠落がないか検査する。
この順なら、schema validation成功だけを理由に途中出力を完了扱いする誤りを避けられます。対象APIやSDK versionで返却形が違うので、実装前に使用しているAPI modeと型を確認してください。
Responses APIの確認順
Responses APIでは、まずresponseのstatusを読みます。公式Structured Outputsガイドには、statusがincompleteで、incomplete_details.reasonがmax_output_tokensの場合にIncomplete responseとして扱う例があります。したがって、output_textがあるか、JSONらしい文字列が取れたかだけで完了を決めず、statusがcompletedかを確認します。incompleteなら、そのreasonをログに残し、出力を最終成果として確定しないでください。
次に、output itemとcontent itemを確認します。Structured Outputsの拒否は、指定したschemaに従わない場合があり、refusal fieldまたはrefusal contentとして示されます。拒否を空データとして扱って成功に見せたり、通常のJSONへ無理に変換したりせず、拒否状態を分けて利用者へ伝えます。content filter等の別理由がある場合も、token上限と同じ修復処理へまとめないでください。
Responsesの判定表
状態・観測|扱い|次の操作
completedで期待するmessage/outputあり|実行完了候補|JSON parse・schema・業務要件を検証
incomplete、reason=max_output_tokens|出力途中の可能性|部分結果を確定せず、出力設計・分割を見直す
refusal contentあり|安全上の拒否|拒否理由を通常データと区別し、別の案内経路へ
outputなし・想定外item|不明状態|SDK/API modeを確認し、raw resultを機密除去して調査
status/errorが不明|完了未確認|成功扱いにせず、request IDや状態を保存して確認
statusがcompletedでも、business completenessは別チェックです。たとえば「10件のレコードを分類する」処理なら、配列長、重複ID、入力IDと出力IDの集合差、各レコードの必須値を確認します。モデルが見つからない項目を「該当なし」としたのか、単に出力しなかったのかも区別します。必要なschemaは値の型を表せても、入力集合すべてを処理したかまでは自動保証しません。
Chat Completionsの確認順
Chat Completionsでは、各choiceのfinish_reasonを確認します。OpenAIのAPI referenceでは、stopは自然な終了またはstop sequence、lengthはrequestで指定された最大token数到達、content_filterはfilterにより省かれた場合、tool_callsはtool呼出しなどを示します。Structured Outputsの例も、finish_reasonがlengthならIncomplete responseとして扱い、refusalを別に分岐しています。
そのため、Chat Completionsで本文を取得できても、finish_reasonがlengthなら通常の完全な回答として保存しません。refusalがある場合はschema parseへ進めず、拒否フローにします。tool_callsやcontent_filterなど、期待する通常回答と異なる終了理由も、アプリの契約に応じた処理へ分岐させます。
API modeを混ぜない
Responsesのstatus/incomplete_detailsとChat Completionsのfinish_reasonは同じフィールドではありません。記事・コード内で両者を一つの汎用プロパティとして扱わないでください。呼び出し元のAPI、SDKのresponse型、使用モデルや機能に合ったfieldを読みます。Streamingを使う場合は、個々のdelta到着と最終eventを混同せず、終了eventが確認できたかも処理契約に含めます。非同期処理の場合は、途中の状態と最終状態を永続化して、完了前に後続処理を開始しない設計にします。
Schema検証の後に必須項目を検証する
構造検証は複数段階で行います。
-
JSON parse:構文として読めるか。
-
Schema validation:object、array、型、許可field、required fieldが定義どおりか。
-
Domain validation:IDが許可された入力集合に属するか、数値範囲が妥当か、状態遷移が許可されるか。
-
Completeness validation:入力件数と出力件数、重複、欠落、空値、未処理理由を照合できるか。
-
Persistence validation:同じ処理を再試行したときに二重登録・二重通知しないか。
たとえばschemaでitemsをarrayにし、各要素にidとcategoryを必須にしても、入力に含まれたidが全て出力に現れたとは限りません。入力IDのsetと出力IDのsetを比較し、欠落・余分・重複を検出します。対象の中に分類不能なものがあるなら、空欄で済ませず、schemaに定義した「要確認」などの状態と根拠を返す設計にします。業務上の許容値や閾値は、モデル任せではなく担当者が決めます。
例:10件の入力を処理する場合
入力側にID A01〜A10があり、responseはstatus completed、JSONはschema-validでも、出力IDがA01〜A09だけなら処理は未完了です。A07が二度含まれるなら、件数が10でも集合の完全性はありません。検査は少なくとも次を行います。
項目|期待|検出する問題
入力ID数|10|入力の読み込み漏れ
出力ID数|10または明示的な除外理由あり|生成途中・項目省略
unique output IDs|10|重複・再試行の二重混入
missing IDs|0、または理由付きreview|未処理項目
unknown IDs|0|入力外の捏造・誤結合
required fields|各IDで存在|schemaだけで検知できない空値
run state|完了|API実行の中断
この例の数字はテスト設計用です。実データの実測ではありません。出力を保存する前に検査結果とAPIのresponse IDを記録します。監査ログには必要なIDと処理状態だけを保存し、機密入力や不要な全文を重複保管しないようにします。
途中で切れたときの選択肢
1. 出力上限・schemaを調整して再実行
Responsesではmax_output_tokens、Chat Completionsではmax_completion_tokensなど、使用APIのrequest fieldとmodel仕様を確認します。値を増やすと解決する場合はありますが、全量出力の上限がなくなるわけではありません。費用・latency・モデル制限も考慮し、上限を上げれば必ず完了すると説明しないでください。
一度の出力に巨大な配列や自由文を詰め込まない方法も検討します。schemaで不要な説明文を省く、必要列だけ返す、配列を小さなchunkへ分けるなどです。ただし、分割で処理対象の重複・欠落が増えるため、chunk ID、入力範囲、続き位置、集約時の重複排除を管理します。
2. 分割・継続する
各chunkに固定IDと件数を付け、部分結果を一時状態にします。完了条件を満たしたchunkだけを確定し、すでに保存されたchunkはidempotency key等で再処理時に二重反映しないようにします。モデルが「続き」を返す場合、前回の最終IDや未処理ID一覧を明示して、境界の曖昧な再開を避けます。
分割後に全体集約する段階でも、chunk数・対象ID集合・結果集合を照合します。途中のchunkでtimeoutやrefusalがあれば、全部成功扱いせず、そのchunkをreviewまたは再実行へ回します。機密データをpromptへ再掲する場合は、必要最小限だけに限定します。
3. 部分結果を人へ返す
一部が利用可能でも、業務上安全に扱える場合に限り、「部分結果」と明示して返せます。自動的に送金、承認、削除、公開などの確定処理へ渡すのは避け、担当者レビューの段階で止めます。結果が部分だと利用者が見落とすUIも危険です。status、処理済み件数、未処理件数、理由、再実行方法を明示し、全件完了と同じ表示を使わないようにします。
実装・テストの最小セット
実装ではAPI modeごとに受け取るresponseを型で扱い、各状態を分岐します。ひとつのcatchでJSON parse error、incomplete、refusal、content filter、network timeoutを同じ「空出力」へ変換しないでください。エラー記録にはrequest ID、API mode、model、status/finish_reason、reason、schema version、対象ID数、出力ID数を含め、API key、本文全文、個人情報を出さないようにします。
最低限のfixtureは次のとおりです。
-
完全なJSON、schema適合、対象IDすべてあり。
-
JSON parseは成功するが、必須業務IDが1つ欠落。
-
schemaのrequired propertyが不足。
-
Responses status=incomplete、reason=max_output_tokens。
-
Chat Completions finish_reason=length。
-
refusal内容がschema外に返る。
-
content filterやtool_callsなど通常終了以外の状態。
-
同じrequestをretryし、同じ結果を二重保存しない。
-
分割出力の最終chunkが欠落している。
-
一部だけ成功した結果をユーザー画面でpartialと表示する。
これらのtestは設計案であり、この原稿を作る際にAPIを呼び出して実行した結果ではありません。導入時は実装と使用SDKのversionに合わせて、失敗後の状態、再開、データ保存境界までテストしてください。
次にすること
途中で切れた回答を見つけたら、最初にどのAPI modeで何が終了理由として返ったかを記録します。Responsesではstatusとincomplete_details、Chat Completionsではfinish_reasonを確認し、refusal・filter・tool callも区別してください。続けてJSON/schema検証、入力IDとの集合照合、二重保存防止を行います。確認が完了するまで、その結果をcompleteとして確定・自動送信しないでください。
Sources & scope
確認日:2026-10-15
OpenAI Structured Outputs: https://developers.openai.com/api/docs/guides/structured-outputs(新しいタブで開く)
OpenAI Chat Completions retrieve reference: https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/retrieve(新しいタブで開く)
公式資料でStructured Outputsのrefusal例、Responsesのincomplete/max_output_tokens判定例、Chat Completionsのfinish_reasonの意味を確認しました。どの返却状態やretryが適用されるかはAPI mode・使用SDK・request設定によります。業務項目のcomplete判定、分割戦略、idempotency設計は実装側で別途定義が必要です。この記事のtest casesは提案であり、live API testを実行した結果ではありません。
