Codex に構造化出力を頼んだ仕事が、生成を始めてから約3秒で「プロセスの失敗」として落ちました。Codex に仕事を分担させる仕組みを技術検証していたときのことです。すぐに落ちたので認証やネットワークを疑いたくなりますが、返っていたのは HTTP 400 の invalid_json_schema で、原因は私たちが送った JSON Schema の properties と required の食い違いでした。
結論
- 私たちが送った schema で
candidates.items.properties.selectionRationaleをrequiredに入れていなかったため、Structured Outputs の受け口が HTTP 400invalid_json_schemaを返していました。 - 認証、利用量の上限、ワーカーの死活(heartbeat)を見ても、おかしな所はありませんでした。認証やネットワークを直しても解けない種類の失敗です。
- 正規の schema で欠けていた
requiredを足しました。あわせて、API へ送る複製だけを再帰的にたどり、すべての object で全 properties をrequiredに入れる形に直しました。 - 保存済みの成果物を手元で検証するときは正規の schema のまま使い、古い版との互換のための optional を残しました。
試した環境
試作した仕組みでは、常駐するワーカーが Codex の CLI を動かし、OpenAI の Structured Outputs で、JSON Schema に合う JSON を返させていました。この記事は、当時の CLI を通した構造化出力の話です。
Codex に頼んだのは、保存済みの調査結果 16 件(そのうち 12 件は候補ごとの結果)を入力にして、1本のまとめを作らせる統合の仕事です。
何が起きたか
統合の仕事を始めると、生成に入った直後に失敗し、私たちの仕組みはこれを「プロセスの失敗」に分類しました。失敗は約3秒で決まり、成果物は残らず、検索も追加の試行もしていません。
そのときの Codex のセッションに絞って調べると、Structured Outputs の受け口が HTTP 400 invalid_json_schema を返していました。送った JSON Schema で、candidates.items.properties.selectionRationale を required に入れていなかったのです。candidates という配列の要素の object で、properties に selectionRationale(候補を選んだ理由)を宣言しながら、required にはその名前を書いていませんでした。
認証でもネットワークでもなかった
開始直後に落ちて、プロセスの失敗という大きな分類で記録される。この見え方は、認証切れやネットワークの障害とよく似ています。ただ、当時の状態を並べると、おかしかったのは送った schema だけでした。
| 見た所 | 当時の状態 |
|---|---|
| 認証 | 正常 |
| 利用量の上限 | 正常 |
| ワーカーの死活(heartbeat) | 正常 |
| 送った JSON Schema | HTTP 400 invalid_json_schema |
この種類の失敗は、認証やネットワークを直しても解けません。プロセスの失敗という分類だけで判断せず、API が返した状態コードとエラーの種類まで見る必要があります。
なぜ properties と required をそろえる必要があったか
私たちの理解では、Codex が使うのは、OpenAI の Structured Outputs が受け付ける JSON Schema の一部です。その範囲では、object に宣言した properties がすべて required に入っている必要があります。同じ範囲では、長さに上限のある配列やスカラーの値の const は受け付けますが、配列の値を持つ const は断ります。
配列の値の const については、以前から手を打っていました。要素の制約で範囲が十分に絞れる場合に限って、送信用の schema から外していたのです。今回、そこに required をそろえる処理を書き足しました。

直し方: 送る schema と手元の schema を分ける
次の図は、直した後の schema の扱いです。正規の schema はそのまま手元の検証に使い、API へ送るときだけ複製を作って直します。
まず、正規の schema で、selectionRationale を required に足しました。
次に、API へ渡す schema の複製だけを再帰的にたどり、object ごとに、宣言した properties の名前をすべて required に入れるようにしました。
function codexCompatibleOutputSchema(schema: JsonObject, depth = 0): JsonObject {
if (depth > 32) throw new CodexRuntimeError("invalid_request");
// …子の schema をたどる処理と、配列の値の const を外す処理(省略)…
const properties = compatible["properties"];
if (isJsonObject(properties)) {
compatible["required"] = Object.keys(properties);
}
return compatible;
}
最後に、正規の schema では、過去に保存した成果物との互換に要る validationPlan、experiencePlan、visualOptions を optional のまま残しました。新しく生成する応答ではこれらも必ず埋めさせますが、手元で古い成果物を検証するときは、無くても通ります。
1つの schema で両方をまかなおうとすると、困ったことになります。送信に合わせて全部を必須にすれば古い成果物が検証で落ち、古い成果物に合わせて optional にすれば API に断られます。生成に使う契約と、過去のデータを検証する契約を分けたので、保存済みの成果物を壊さずに直せました。
あわせて、23 種類の仕事、12 の最上位の schema、63 の object を調べました。どの object も closed(宣言していないプロパティを許さない形)になっていることも確かめています。
どう確かめたか
直した後に同じ保存済みの入力 16 件でやり直すと、72,032 バイトのまとめを保存でき、selectionRationale が抜けた候補は 0 件でした。
確かめていないこと
- いまの OpenAI の製品で同じ制約かどうかは確かめていません。また、ここで示した変換は、あらゆる JSON Schema に使える互換の変換として保証するものではありません。
- optional の項目を機械的に必須にしても、意味が保てるとは限りません。今回の変換は、私たちの仕組みの生成の契約と、古い成果物との互換に合わせて行ったものです。
- schema を直して生成が通っても、まとめの中身が良いとは限りません。
おわりに
構造化出力が開始直後に落ちたら、認証やネットワークを疑う前に、API が返した状態コードとエラーの種類を見てください。invalid_json_schema なら、まずは送った schema の properties と required を見比べてみてください。
※ OpenAI の Structured Outputs が受け付ける JSON Schema の範囲や Codex CLI の振る舞いは執筆時点の情報で、いまは変わっている可能性があります。

