モデルの出力をデータベース、フォーム、下流の Agent に接続するなら、「JSON で返してください」とプロンプトに書くだけでは足りません。OpenAI Structured Outputs は制約付きデコードにより、出力を指定した JSON Schema に固定します。フィールドの欠落、enum の捏造、文字列と数値の型ゆれを防げます。抽出、チケット分類、評価スコア、多段ワークフローを実装するエンジニア向けに、JSON Mode から strict: true までの移行手順を一通り解説します。
JSON Mode では安定しない理由
JSON Mode(type: "json_object")が保証するのは「JSON パーサーが読めること」だけです。キー名、必須フィールド、enum の集合、数値型までは保証しません。本番でよく起きる失敗は次の 3 つです。severity の欠落、42 が "42" になること、enum の外に urgent-plus を作り出すことです。下流が強い型でパースすると、ランダムなサンプルでパイプライン全体が止まります。
| 能力 | JSON Mode | Structured Outputs |
|---|---|---|
| 合法な JSON を出力する | はい | はい |
| 指定した JSON Schema を守る | いいえ(プロンプト頼み) | はい(制約付きデコード) |
| 有効化の方法 | json_object |
json_schema + strict: true |
| 代表的なモデル | 比較的初期の GPT-4o / GPT-3.5 など | gpt-4o-2024-08-06、gpt-4o-mini およびそれ以降のスナップショット |
| 拒否応答の扱い | JSON に見える拒否文が返ることがある | 独立した refusal フィールドを使う |
公式は Structured Outputs を JSON Mode の進化と位置づけています。新規プロジェクトでは、パース失敗後に 3 回リトライするのではなく、最初から Schema を使うべきです。モデルごとの Token 課金を比較するなら、Kimi K3 と GPT-5.5 の API コスト比較も参照してください。構造化出力そのものが出力サイズを大きく増やすことはほとんどなく、請求を押し上げるのはリトライと過剰な推論です。
response_format に置き、Responses API では text.format に置きます。フィールドのパスは異なりますが、strict、additionalProperties: false、および「すべて required」といった制約は共通です。
最小の実行可能なリクエスト
次のチケット抽出 Schema は、本番でよく使う要件の大半をカバーします。文字列、enum、整数、そして null のユニオン型で任意フィールドを表現します。モデルはすべてのキーを返す必要があります。アカウント ID がない場合はキーを省略せず、明示的に null を返します。
{
"model": "gpt-4o-2024-08-06",
"messages": [
{"role": "system", "content": "ユーザーの説明からサポートチケットを抽出してください。"},
{"role": "user", "content": "保存済みカードで決済ページが 500 になります。今日から発生。アカウント 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": { "type": ["string", "null"] }
},
"required": ["summary", "category", "severity", "account_id"],
"additionalProperties": false
}
}
}
}
Responses API では、同じ Schema を text.format に入れ、type、name、schema、strict を兄弟フィールドとして並べます。ある Schema を初めて使うときは、サーバー側で制約をコンパイルするため遅延がやや増えることがあります。その後は同一 Schema がキャッシュされ、通常の遅延に戻ります。
strict Schema が満たすべき規則
strict: true を有効にすると、提出するのは「緩い JSON Schema」ではなく、公式がサポートするサブセットです。準拠しない場合は「できるだけ守る」ではなく、そのまま 400 になります。
- ルートは object でなければならない:ルートに
anyOfや配列は使えません。Zod のdiscriminatedUnionがトップレベルのanyOfを生成する場合は、{ "result": ... }のようにオブジェクトで包みます。 - すべての object に
additionalProperties: falseが必要:ネストしたオブジェクトも含みます。1 つ漏れると拒否されます。 propertiesに出したキーはすべてrequiredに入れる:任意の意味は"type": ["string", "null"]か、anyOfにnullを加えて表現します。- サポートされる型:string、number、integer、boolean、object、array、enum、
anyOf。 - 文字列の制約:
pattern、format(email、date-time、uuid、ipv4など)。 - 数値と配列:
minimum/maximum/multipleOf、minItems/maxItems。 - 明示的に未サポート:
allOf、not、if/then/else、dependentRequiredなどの組み合わせキーワード。 - 規模の上限:オブジェクト属性は合計で約 5000、ネストは 10 階層まで。属性名、定義名、enum 値の文字列合計は 12 万文字以内。enum 値の総数は最大 1000。
pattern、format、minLength、minimum、minItems などの制約が現時点でも未対応のことがあります。まず基盤スナップショットで Schema を検証してから、ファインチューンの要否を判断してください。
ツール呼び出しと返信本文
同じ strict は関数 / ツール引数にも使えます。違いは意図です。ツールの Schema は「モデルが何を呼び出すか」を記述し、response_format / text.format は「ユーザーへ返す形」を記述します。抽出、採点、UI 状態の生成は後者、天気の照会、ファイル書き込み、コマンド実行は前者です。ターミナル Agent と GUI 操作の能力を比較するなら、Claude Code と OpenAI Computer Use の関係を参照してください。あちらは「どう行動するか」の層で、本稿が扱うのは「行動の前後にあるデータ契約」です。
SDK でオブジェクトへ直接パースする
JSON Schema を手書きすると required を忘れがちです。公式 SDK には Pydantic / Zod のヘルパーがあり、型から Schema を生成し、応答側ではパース済みオブジェクトを直接受け取れます。以下は Python の典型的な書き方です。
from typing import Literal, Optional
from pydantic import BaseModel
from openai import OpenAI
class SupportTicket(BaseModel):
summary: str
category: Literal["billing", "bug", "account", "other"]
severity: int
account_id: Optional[str]
client = OpenAI()
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "チケットを抽出してください。"},
{"role": "user", "content": "決済ページが 500、アカウント acct_8842。"},
],
response_format=SupportTicket,
)
ticket = completion.choices[0].message.parsed
if completion.choices[0].message.refusal:
raise RuntimeError(completion.choices[0].message.refusal)
print(ticket.category, ticket.severity)
エラー、拒否応答、実装チェックリスト
本番投入前にこのリストを通せば、「ローカルの curl は通るのにパイプラインだけ 400」という問題の大半を消せます。
- 400 Unsupported schema:
additionalProperties: falseの欠落、required でないフィールド、ルートがanyOf、allOfの使用。 - モデルの拒否:安全ポリシーが発動すると、内容は Schema に入りません。
refusalを読み、UI に表示してください。JSON パース失敗としてリトライしないでください。 - 任意フィールドが空文字になる:契約に
nullと書いたならnullを受け取ります。DB 投入前に SQL のNULLへ写し、推測で埋めないでください。 - enum が広すぎる:業務状態は 5〜8 個に絞ります。数百個の enum はコンテキストを消費し、1000 件の上限にも近づきます。
- キーの順序:出力キーの順は Schema の宣言順と一致します。ストリーミング受信時はフィールド単位で増分パースできます。
- プロンプトは依然として有効:Schema は形、プロンプトは意味を担います。「severity は 1〜5、5 は全サイト障害」は system に書き、整数範囲は
minimum/maximumでさらに固定できます。
運用では次の 3 点を固定することを推奨します。Schema を Git に入れること。拒否、400、タイムアウトを別々に計測すること。玩具入力ではなく、実チケットで回帰サンプルを持つことです。パース層が安定して初めて、分類精度と遅延に集中でき、毎週正規表現を直す作業から離れられます。
Mac mini で抽出パイプラインを回すと、環境の手間が減ります
Structured Outputs のデバッグは短い循環です。Schema を直し、少量のサンプルを流し、パース済みオブジェクトを確認します。macOS では Python、Node.js、Docker、Homebrew がそのまま使え、先に WSL を組む必要がありません。Apple Silicon のユニファイドメモリなら、エディタ、ローカル検証スクリプト、長コンテキスト評価を同時に開いても、型チェックの最中にマシンが swap へ落ちにくくなります。
Mac mini M4 の待機電力はおよそ 4W で、回帰サンプルを一晩走らせる用途に向きます。macOS はクラッシュ率が低く、Gatekeeper と SIP により依存スクリプトが汚染されるリスクも下がります。同価格帯の Windows 機と比べ、無人で Agent 評価を回し続ける場合は安定性と電気代の両方で有利です。
JSON 回帰とモデル比較を常時動かすノードが必要なら、Kvmzen のプランを確認すると、Schema 契約をノート PC から固定スペックのクラウド Mac へ移せます。