Gemini APIのJSON出力・構造化出力で起きる問題とは
Gemini APIのJSON Modeや**Structured Output(構造化出力)**は、AIの応答を機械的に処理しやすいJSON形式で受け取るための強力な機能です。しかし、実際に本番アプリケーションで利用していると「JSONがパースできない」「スキーマ通りの値が返らない」「レスポンスが途中で切れている」といった問題に直面することがあります。
コード例はすべてPythonとJavaScript(Node.js)の両方で示しているため、お使いの環境に合わせてすぐに適用できます。
症状別:よくあるJSON出力エラー5パターン
Gemini APIの構造化出力に関するトラブルは、大きく5つのパターンに分類できます。まずは自分が遭遇している問題がどれに該当するかを確認しましょう。
パターン1:レスポンスがJSON形式ではない
response_mime_type を application/json に設定しているにもかかわらず、プレーンテキストやMarkdown形式で応答が返ってくるケースです。特にGemini 1.5系からGemini 2.5系へ移行した直後に発生しやすい問題です。
パターン2:JSONの構文エラー(パースできない)
レスポンスは一見JSON形式に見えるものの、JSON.parse() や json.loads() で例外が発生するパターンです。余分なコードブロック記号(```)が付与されていたり、末尾にカンマが残っていたりすることが原因です。
パターン3:スキーマ定義と異なる構造が返る
response_schema で指定したフィールドが欠損している、型が異なる(数値を期待しているのに文字列で返る)、enum値が守られないなどの問題です。
パターン4:レスポンスが途中で切れている(Truncated JSON)
長い出力を要求した際に、JSONオブジェクトが閉じきらないまま finish_reason が MAX_TOKENS になるパターンです。配列の要素を大量に生成する場合に特に起きやすい問題です。
パターン5:空のレスポンスまたはnullが返る
スキーマを指定しているにもかかわらず、空文字列や null が返ってくるパターンです。セーフティフィルターによるブロックが原因であることが多いです。
原因の分析:なぜJSON出力が失敗するのか
各パターンの根本原因を理解することが、確実な解決への第一歩です。
response_mime_type の設定ミス
最も基本的な原因は、リクエスト時に response_mime_type が正しく設定されていないことです。SDKのバージョンによってパラメータ名が異なる場合もあるため注意が必要です。
# ❌ 間違い:generation_config に含めていない
model = genai.GenerativeModel("gemini-2.5-flash")
response = model.generate_content("ユーザーデータをJSON形式で返してください")
# ✅ 正しい:generation_config で明示的に指定
model = genai.GenerativeModel(
"gemini-2.5-flash",
generation_config=genai.GenerationConfig(
response_mime_type="application/json"
)
)
response = model.generate_content("ユーザーデータをJSON形式で返してください")
print(response.text)
# 期待出力: {"name": "田中太郎", "age": 30, "email": "tanaka@example.com"}スキーマ定義の不備
response_schema に渡すスキーマが不完全だと、モデルは自己判断で構造を補完しようとします。これがスキーマ違反の主な原因です。
# ❌ 不十分なスキーマ(型指定なし)
schema = {"type": "object", "properties": {"score": {}}}
# ✅ 完全なスキーマ(型・必須フィールド・説明を明記)
schema = {
"type": "object",
"properties": {
"score": {
"type": "number",
"description": "0から100までの評価スコア"
},
"category": {
"type": "string",
"enum": ["positive", "negative", "neutral"],
"description": "感情カテゴリ"
}
},
"required": ["score", "category"]
}max_output_tokens の不足
デフォルトのトークン数制限では、複雑なJSON構造を完全に出力しきれない場合があります。特に配列を含む出力で頻発します。
セーフティフィルターによるブロック
入力内容がセーフティポリシーに抵触すると、モデルはJSON出力を生成する前にリクエストをブロックし、空のレスポンスや null を返します。
解決手順:Step by Stepガイド
Step 1:SDKとモデルバージョンを確認する
まず、使用しているSDKが最新であること、そしてJSON Modeをサポートしているモデルを使用していることを確認します。
# Python SDK のバージョン確認と更新
pip install --upgrade google-genai
python -c "import google.genai; print(google.genai.__version__)"// Node.js SDK のバージョン確認
const { GoogleGenAI } = require("@google/genai");
console.log("SDK loaded successfully");
// package.json で最新バージョンを確認
// npm install @google/genai@latestJSON Mode / Structured Output をサポートしているモデルは以下の通りです。
- Gemini 2.5 Pro — 完全対応(推奨)
- Gemini 2.5 Flash — 完全対応(コスト重視の場合に推奨)
- Gemini 2.0 Flash — 対応
- Gemini 1.5 Pro / Flash — 基本対応(一部制約あり)
Step 2:response_mime_type と response_schema を正しく設定する
JSON出力を確実に得るための最も重要な設定です。
import google.genai as genai
client = genai.Client(api_key="YOUR_API_KEY")
# スキーマ定義
product_schema = {
"type": "object",
"properties": {
"products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "商品名"},
"price": {"type": "integer", "description": "価格(円)"},
"in_stock": {"type": "boolean", "description": "在庫あり"}
},
"required": ["name", "price", "in_stock"]
}
}
},
"required": ["products"]
}
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="以下の3商品の情報を生成してください:ノートPC、マウス、キーボード",
config=genai.types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=product_schema,
max_output_tokens=2048
)
)
import json
data = json.loads(response.text)
print(json.dumps(data, ensure_ascii=False, indent=2))
# 期待出力:
# {
# "products": [
# {"name": "ノートPC", "price": 89800, "in_stock": true},
# {"name": "マウス", "price": 3980, "in_stock": true},
# {"name": "キーボード", "price": 7980, "in_stock": false}
# ]
# }const { GoogleGenAI } = require("@google/genai");
const ai = new GoogleGenAI({ apiKey: "YOUR_API_KEY" });
const productSchema = {
type: "object",
properties: {
products: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string", description: "Product name" },
price: { type: "integer", description: "Price in JPY" },
in_stock: { type: "boolean", description: "In stock" }
},
required: ["name", "price", "in_stock"]
}
}
},
required: ["products"]
};
async function generateProducts() {
const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: "Generate info for 3 products: laptop, mouse, keyboard",
config: {
responseMimeType: "application/json",
responseSchema: productSchema,
maxOutputTokens: 2048
}
});
const data = JSON.parse(response.text);
console.log(JSON.stringify(data, null, 2));
}
generateProducts();Step 3:max_output_tokens を十分に確保する
JSON出力が途中で切れる場合は、トークン上限を引き上げます。
config = genai.types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=product_schema,
max_output_tokens=8192 # デフォルトより大きく設定
)出力トークン数の目安として、JSON1文字あたり約1〜2トークンを消費します。1,000フィールド規模のJSONを期待する場合は max_output_tokens を 8192 以上に設定してください。
Step 4:レスポンスのバリデーションを実装する
モデルの出力を盲目的に信頼せず、受け取り側で必ずバリデーションを行います。
import json
def validate_and_parse(response_text, expected_keys=None):
"""Gemini APIレスポンスのJSON安全パーサー"""
# コードブロック記号の除去
text = response_text.strip()
if text.startswith("```"):
text = text.split("\n", 1)[1] if "\n" in text else text[3:]
if text.endswith("```"):
text = text[:-3]
text = text.strip()
try:
data = json.loads(text)
except json.JSONDecodeError as e:
print(f"JSON parse error: {e}")
# 末尾カンマの修復を試みる
import re
text = re.sub(r',\s*([}\]])', r'\1', text)
try:
data = json.loads(text)
print("Recovered after removing trailing commas")
except json.JSONDecodeError:
raise ValueError(f"Unrecoverable JSON: {text[:200]}...")
# 期待するキーの存在チェック
if expected_keys:
missing = [k for k in expected_keys if k not in data]
if missing:
raise ValueError(f"Missing keys: {missing}")
return data
# 使用例
result = validate_and_parse(response.text, expected_keys=["products"])
print(f"取得した商品数: {len(result['products'])}")Step 5:finish_reason を確認してリトライする
レスポンスが不完全な場合、finish_reason で原因を特定し、適切にリトライします。
def generate_with_retry(client, prompt, schema, max_retries=3):
"""finish_reason に基づく自動リトライ"""
for attempt in range(max_retries):
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
config=genai.types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=schema,
max_output_tokens=8192
)
)
# finish_reason の確認
candidate = response.candidates[0]
finish_reason = candidate.finish_reason
if finish_reason == "STOP":
# 正常終了 — JSONパースを試行
try:
return json.loads(response.text)
except json.JSONDecodeError:
print(f"Attempt {attempt + 1}: JSON parse failed, retrying...")
continue
elif finish_reason == "MAX_TOKENS":
print(f"Attempt {attempt + 1}: Output truncated, increasing tokens...")
# トークン数を増やしてリトライ
continue
elif finish_reason == "SAFETY":
print(f"Attempt {attempt + 1}: Blocked by safety filter")
# プロンプトの修正が必要
raise ValueError("Safety filter blocked the response")
else:
print(f"Attempt {attempt + 1}: Unexpected finish_reason: {finish_reason}")
continue
raise RuntimeError(f"Failed after {max_retries} retries")確認方法:解決できたかの検証手順
修正を適用した後、以下の手順で問題が解決したことを確認します。
検証1:基本的なJSON出力テスト
# 最小限のテストケースで確認
test_response = client.models.generate_content(
model="gemini-2.5-flash",
contents="名前と年齢のサンプルデータを1件生成してください",
config=genai.types.GenerateContentConfig(
response_mime_type="application/json",
response_schema={
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
)
)
data = json.loads(test_response.text)
assert isinstance(data["name"], str), "name should be string"
assert isinstance(data["age"], int), "age should be integer"
print("✅ Basic JSON test passed")検証2:大量データの出力テスト
配列を含む大きなJSONが途中で切れないことを確認します。
large_response = client.models.generate_content(
model="gemini-2.5-flash",
contents="日本の都道府県47件すべてのデータを生成してください",
config=genai.types.GenerateContentConfig(
response_mime_type="application/json",
response_schema={
"type": "object",
"properties": {
"prefectures": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"capital": {"type": "string"}
},
"required": ["name", "capital"]
}
}
},
"required": ["prefectures"]
},
max_output_tokens=8192
)
)
data = json.loads(large_response.text)
print(f"✅ Large data test: {len(data['prefectures'])} prefectures returned")検証3:finish_reason の確認
candidate = large_response.candidates[0]
assert candidate.finish_reason == "STOP", f"Unexpected: {candidate.finish_reason}"
print("✅ finish_reason is STOP (complete response)")予防策:再発防止のベストプラクティス
スキーマは必ず完全に定義する
すべてのフィールドに type、description、required を明記します。曖昧さがあるとモデルの解釈に依存してしまい、出力が不安定になります。
レスポンスバリデーションを本番コードに組み込む
AIモデルの出力は原理的に非決定的です。try/except と型チェックを必ず実装し、パース失敗時のフォールバック処理を用意してください。
出力が大きくなる場合はチャンク分割する
47都道府県のように件数が多いデータを一度に要求するのではなく、「最初の10件」「次の10件」のように分割してリクエストすることで、途中切れのリスクを大幅に減らせます。
System Instructionsで出力形式を補強する
response_mime_type と response_schema に加えて、System Instructions で「必ず有効なJSONのみを返してください。コードブロック記号は付けないでください」と明記すると、不正な出力の発生率がさらに下がります。
SDKを定期的にアップデートする
Google AI Python SDKは頻繁にアップデートされており、構造化出力に関するバグ修正も多く含まれています。pip install --upgrade google-genai を定期的に実行しましょう。
全体を振り返って
Gemini APIのJSON出力・構造化出力のトラブルは、response_mime_type と response_schema の正しい設定、十分な max_output_tokens の確保、そしてレスポンスバリデーションの実装によって、ほとんどのケースを解決できます。本記事で紹介した5つのエラーパターンと対処法を参考に、安定したJSON出力を実現してください。
エラーハンドリング全般についてはGemini API エラーハンドリング完全ガイドも併せてご覧ください。また、JSON Modeの基礎から学びたい方はGemini API の JSON Mode と構造化出力ガイドが参考になります。