Structured OutputsでJSON Schemaに一致する出力を得られるなら、アプリ側のvalidationも消せそうに見えます。消せるのは、主に「JSONとして壊れている」「必須キーがない」「enum外の値が来る」といった形式上の失敗です。
start_dateよりdue_dateが前、入力に存在しない依頼者名が補完される、といった誤りはSchemaを通過できます。Schemaを厳しくするほど楽になるのは構造の検証であって、業務上の正しさの検証ではありません。
2026年9月30日時点のOpenAI公式ドキュメントでも、Structured Outputsは指定したJSON Schemaへの準拠を保証する仕組みとして説明される一方、生成内容そのものには誤りが残り得ると明記されています。
JSON modeで残る失敗を分離する
まず、問題を3種類に分けます。
たとえば、作業依頼を次の形へ変換するとします。
{
"title": "在庫表を確認する",
"priority": "high",
"start_date": "2026-10-05",
"due_date": "2026-10-08"
}
ここで起きる失敗は同じではありません。
- JSONが途中で切れるなど、そもそもparseできない
-
priorityが想定外、キーが欠けるなど、構造が違う -
due_dateがstart_dateより前など、構造は正しいが意味がおかしい
JSON modeが主に扱うのは1です。
OpenAIのStructured model outputsでは、JSON modeはvalid JSONを保証する一方、特定のSchemaへの準拠は保証しないとされています。
つまり、こんなJSONは普通にparseできます。
import json
raw = """
{
"title": "在庫表を確認する",
"priority": "urgent",
"start_date": "2026-10-05"
}
"""
data = json.loads(raw)
print(data)
しかしアプリが期待しているpriorityがlow | medium | highで、due_dateが必須なら、このデータは使えません。
ここでStructured Outputsを入れる意味があります。
strictなSchemaが消してくれるもの
Responses APIではStructured Outputsをtext.formatで指定できます。公式ドキュメントでは、Structured Outputsが利用できる場合はJSON modeよりこちらを使うことが推奨されています。
2026年9月30日時点の公式ドキュメントに合わせ、Pythonで最小構成を書くと次のようになります。
依存はopenaiです。APIの仕様変更を追えるよう、実際のプロジェクトでは動作確認したバージョンを固定してください。
openai
import json
from openai import OpenAI
client = OpenAI()
schema = {
"type": "object",
"properties": {
"title": {"type": "string"},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
},
"start_date": {
"type": "string",
"format": "date",
},
"due_date": {
"type": "string",
"format": "date",
},
},
"required": [
"title",
"priority",
"start_date",
"due_date",
],
"additionalProperties": False,
}
response = client.responses.create(
model="gpt-6-astra",
instructions="入力文から作業依頼を抽出してください。",
input=(
"10月5日に在庫表の確認を開始し、"
"10月8日までに終える。優先度は高。"
),
text={
"format": {
"type": "json_schema",
"name": "work_request",
"strict": True,
"schema": schema,
}
},
)
data = json.loads(response.output_text)
print(data)
ここではSchemaが責任を持つ範囲がかなり明確です。
priorityは3種類に限定されます。4つのプロパティは必須です。余計なプロパティも許しません。日付文字列にはformat: "date"を指定しています。
なおStructured Outputsのstrict modeでは、すべてのフィールドをrequiredにし、objectにはadditionalProperties: falseを設定する必要があります。optional相当の値が必要なら、公式ドキュメントではnullとのunionで表現する方法が案内されています。
これなら、LLMが返したJSONを受け取るたびに、
if "due_date" not in data:
...
のような防御コードをあちこちへ足す必要はありません。
Schemaは「アプリが受け取れる形」をLLMとの境界で固定するために使う、と考えると扱いやすいです。
ただし、ここでvalidationを全部削除すると別の穴が残ります。
Schemaを通っても壊れているデータ
次の値を見てください。
{
"title": "在庫表を確認する",
"priority": "high",
"start_date": "2026-10-08",
"due_date": "2026-10-05"
}
JSONとして正しい。型も正しい。必須項目もあります。enumにも違反していません。日付もそれぞれ単独では妥当です。
それでも「開始日より期限が前」という業務ルールを採用するシステムなら不正です。
Schemaで各フィールドを厳しくしても、複数フィールドの関係から決まる不変条件は別問題です。
さらに厄介なのが「根拠」の問題です。
入力が、
在庫表を来週確認しておいて。
だけだったとします。
出力が、
{
"title": "在庫表を確認する",
"priority": "high",
"start_date": "2026-10-05",
"due_date": "2026-10-08"
}
ならSchema上は問題ありません。しかし入力に優先度や具体的な期限がなければ、それらの値を確定してよいかはアプリ側の仕様次第です。
OpenAIのStructured Outputs公式ドキュメントにも、ユーザー入力がSchemaに適合する回答を作れない場合、モデルがSchemaへ合わせようとして値を生成する可能性があるため、その場合の扱いを指示するよう注意があります。
これはSchemaを複雑にすれば解決する話ではありません。
Schemaが見ているのは出力です。「その値が入力のどこに根拠を持つか」は、別の情報を設計しない限り判定できません。
domain validatorを別層に置く
そこで、構造検証の後ろに業務ルール専用のvalidatorを置きます。
API呼び出しなしでも確認できる最小コードにすると、こうなります。Python 3.11以降で動かせます。
from datetime import date
from typing import Literal, TypedDict
class WorkRequest(TypedDict):
title: str
priority: Literal["low", "medium", "high"]
start_date: str
due_date: str
def validate_domain(req: WorkRequest) -> None:
start = date.fromisoformat(req["start_date"])
due = date.fromisoformat(req["due_date"])
if due < start:
raise ValueError(
"due_date must be on or after start_date"
)
valid: WorkRequest = {
"title": "在庫表を確認する",
"priority": "high",
"start_date": "2026-10-05",
"due_date": "2026-10-08",
}
invalid: WorkRequest = {
"title": "在庫表を確認する",
"priority": "high",
"start_date": "2026-10-08",
"due_date": "2026-10-05",
}
validate_domain(valid)
try:
validate_domain(invalid)
except ValueError as e:
print(e)
出力は次です。
due_date must be on or after start_date
ポイントは、これを「LLMの出力をもう一度parseする処理」と考えないことです。
たとえばPydanticのモデルからJSON Schemaを生成し、そのモデルへ戻す構成自体は便利です。公式ドキュメントでもSDKのSchema helperを使い、プログラミング言語側の型とJSON Schemaが乖離しないようにする方法が推奨されています。
ただ、型へparseできたことと、業務上受理できることは同義ではありません。
私は業務システムやAIを使った自動化を考えるとき、AIの出力をそのまま確定値として扱わない設計を大切にしています。このケースも同じで、境界を次のように分けると整理しやすいです。
入力
↓
LLM
↓
Structured Outputs
↓
型・必須項目・enumなどの構造保証
↓
domain validator
↓
日付関係・入力根拠・業務上の不変条件
↓
後続処理
入力根拠まで検査したいなら、出力Schemaそのものを変える方法もあります。たとえば抽出値だけでなく、その値が明示されていたかを表すフィールドを持たせます。
{
"priority": "high",
"priority_source": "explicit"
}
ただしpriority_sourceがexplicitであること自体もモデルの出力です。高い信頼性が必要なら、原文の該当範囲を保持してアプリ側で照合する、未確定値をnullにして人が確定する、といった設計まで考える必要があります。
ここは「Schemaをどこまで厳しくできるか」ではなく、「何を機械的に保証でき、何を業務判断として残すか」の境界です。
Structured Outputsを導入した結果、validatorがゼロになることを目標にすると設計を誤りやすいです。構造のための再試行や防御コードを減らし、その代わりdomain validatorには業務上の不変条件だけを残す。この分離なら、Schemaを厳しくした効果がコードにもテストにも表れます。
適用できないのは、正しさ自体をコードで判定できない処理です。要約の品質や自由記述の妥当性のような問題は、日付の前後関係と同じvalidatorでは扱えません。その場合はSchemaの追加ではなく、評価方法、人による確認、根拠情報の保持を別途設計する必要があります。

