構造化出力(JSON)で確実にパースする:response_formatの使い方
LLMの出力をプログラムで使いたいとき、プロンプトで「JSONで返して」とお願いするのは最後の手段です。 response_format にJSON Schemaを渡せば、スキーマに沿ったJSONが返ってきます。
違いは信頼性です。プロンプト頼みだと、9割は正しいJSONが返り、残り1割で ```json のコードブロックに包まれたり、前置きの一文が付いたり、末尾のカンマが混ざったりします。その1割を潰すために書くパース処理が、いちばん壊れやすいコードになります。
response_formatを使う
response_format に json_schema を指定します。
from openai import OpenAI
client = OpenAI(
api_key="<FASTMETAL_API_KEY>",
base_url="https://api.fastmetal.ai/v1",
)
resp = client.chat.completions.create(
model="gemini-flash-lite-free",
max_tokens=300,
messages=[
{"role": "user", "content": "「新宿の田中太郎さん(42歳)」から情報を抽出して"},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "person",
"strict": True,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"},
},
"required": ["name", "age", "city"],
"additionalProperties": False,
},
},
},
)
print(resp.choices[0].message.content)
返ってきた content は、そのまま json.loads() に通せます(2026年8月29日実測)。
{
"name": "田中太郎",
"age": 42,
"city": "新宿"
}
コードブロックの記号も、前置きの文章もありません。
curlの場合
curl https://api.fastmetal.ai/v1/chat/completions \
-H "Authorization: Bearer $FASTMETAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-flash-lite-free",
"max_tokens": 300,
"messages": [
{"role": "user", "content": "「新宿の田中太郎さん(42歳)」から情報を抽出して"}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"],
"additionalProperties": false
}
}
}
}'
スキーマの書き方で押さえる3点
required に全項目を入れる
「任意項目」にすると、モデルはその項目を省略できてしまいます。省略を許したいなら、required に入れたうえで型を ["string", "null"] にして、値としてのnullを返させるほうが扱いやすくなります。キーの有無を毎回チェックするコードを書かずに済みます。
additionalProperties: false を付ける
付けないと、スキーマにない項目を勝手に足されることがあります。
strict: true を付ける
スキーマへの準拠を厳密に扱わせる指定です。
description を各プロパティに書けるのも重要です。{"type": "string", "description": "都道府県名。市区町村は含めない"} のように書くと、抽出の粒度が安定します。スキーマはバリデーションであると同時に指示でもあると考えてください。
実用的な例:レビューの分類
配列とenumを使うと、分類タスクがそのまま構造化されます。
schema = {
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"],
},
"topics": {
"type": "array",
"items": {"type": "string"},
"description": "言及されている話題。最大3つ",
},
"summary": {"type": "string", "description": "20字以内の要約"},
},
"required": ["sentiment", "topics", "summary"],
"additionalProperties": False,
}
resp = client.chat.completions.create(
model="gemini-flash-lite-free",
max_tokens=300,
messages=[
{"role": "system", "content": "レビューを分類するアシスタントです。"},
{"role": "user", "content": "配送は早かったけど、箱が潰れていた。商品自体は満足。"},
],
response_format={
"type": "json_schema",
"json_schema": {"name": "review", "strict": True, "schema": schema},
},
)
import json
data = json.loads(resp.choices[0].message.content)
print(data["sentiment"], data["topics"])
enum を使えば、分類ラベルの表記ゆれ(positive / Positive / ポジティブ)が起きません。分岐する側のコードが単純になります。
それでもパースが失敗する場合
構造化出力でも、失敗しうるパターンが2つあります。
1. max_tokens で途中で切れる
出力の途中で上限に達すると、JSONが閉じないまま返ります。これはスキーマの問題ではないので、構造化出力でも防げません。
finish_reason を必ず確認してください。
choice = resp.choices[0]
if choice.finish_reason == "length":
raise RuntimeError("max_tokens で切れました。上限を上げてください")
data = json.loads(choice.message.content)
finish_reason が stop なら正常終了、length なら切れています。この見分け方はChat Completions APIとはでも触れています。
配列を返させるスキーマは特に危険です。件数の上限をスキーマの description に書き、max_tokens に余裕を持たせてください。
2. モデルが対応していない
すべてのモデルが json_schema に対応しているわけではありません。非対応の場合、指定が無視されて普通の文章が返ることがあります。まず1回叩いて確かめてください。
保険としてのパース処理を1枚挟んでおくと安全です。
import json, re
def parse_json(text: str):
try:
return json.loads(text)
except json.JSONDecodeError:
# ```json ... ``` に包まれている場合の救済
m = re.search(r"```(?:json)?\s*(.+?)\s*```", text, re.S)
if m:
return json.loads(m.group(1))
raise
これは非対応モデルへのフォールバックであって、通常の経路ではありません。毎回ここを通っているなら、モデルか指定を見直すサインです。
json_object との違い
response_format にはもう一つ {"type": "json_object"} という指定があります。これは「JSONであること」だけを保証し、中身の構造は保証しません。キー名が毎回変わる可能性があります。
構造まで決めたいなら json_schema を使ってください。json_object を使う場合は、期待するキーをプロンプト側に書く必要があります。
構造化出力とツール呼び出しの使い分け
似て見えますが、目的が違います。
| 構造化出力 | ツール呼び出し | |
|---|---|---|
| 目的 | 出力の形を固定する | 外部の処理を実行する |
| 往復 | 1回 | 2回以上 |
| 使う場面 | 抽出、分類、要約の構造化 | 検索、DB参照、副作用のある操作 |
「JSONが欲しいだけ」なら構造化出力です。 ツール呼び出しでJSONを取り出す実装を見かけますが、往復が増えるぶん遅く、高くつきます。
つまずきやすい点
required を絞ってしまう
任意項目はキーごと消えます。nullを返させるほうが扱いやすくなります。
finish_reason を見ていない
length で切れたJSONは必ずパースに失敗します。エラーの原因を探す前にここを見てください。
additionalProperties を省略する
スキーマにないキーが増えることがあります。
スキーマが深すぎる 入れ子が深いと精度が落ち、出力トークンも増えます。2〜3階層に収め、必要なら複数回に分けてください。
プロンプト側の指示と矛盾している 「JSONで返して」とプロンプトに書きつつスキーマも渡すと、指示が二重になります。スキーマに任せて、プロンプトには何を抽出するかだけを書いてください。
次のステップ
画像から情報を抽出して構造化する、という組み合わせは実用的です。画像入力の使い方と併せて使うと、レシートや帳票の読み取りがそのまま実装できます。
複数モデルの出力品質を比べたい場合は、構造化出力にしておくと自動評価が書きやすくなります。Evalsの入門記事を参照してください。
よくある質問
Q. プロンプトで「JSONで返して」と書くのとどう違いますか?
プロンプト頼みだと、コードブロックに包まれたり前置きが付いたりして、一定の確率でパースに失敗します。response_format にスキーマを渡せば、スキーマに沿ったJSONが直接返ります。
Q. 構造化出力は無料モデルでも使えますか?
使えます。この記事のコードは gemini-flash-lite-free で動作を確認しています。ただしすべてのモデルが対応しているわけではないので、使うモデルで一度確かめてください。
Q. JSONが途中で切れてパースに失敗します。
max_tokens の上限で切れている可能性が高いです。finish_reason が length になっていないか確認し、上限を上げてください。構造化出力でもこれは防げません。
Q. json_object と json_schema はどちらを使うべきですか?
構造まで固定したいなら json_schema です。json_object はJSONであることしか保証せず、キー名は変わりえます。
まとめ
response_format に json_schema を渡せば、パース処理を書かずに済みます。required は絞らずnullを許す、additionalProperties: false を付ける、finish_reason を必ず見る——この3つで実務上の失敗はほぼ潰せます。
無料モデルで試せるので、APIキーを取得して手元のスキーマで1回叩いてみてください。対応モデルはモデルカタログにあります。