GEMINI LABEN
DEPRECATION — 画像プレビューモデル2種が本日6/25に停止。利用中の自動化・スクリプトは本日中の移行が必須ですGA — 入れ替わりでgemini-3.1-flash-imageとgemini-3-pro-imageがネイティブ画像モデルの正式版になりましたMEDIA — 動画→画像生成に対応。動画を文脈として渡し高品質なサムネイル等を生成できます(3.1 flash image限定)AUDIO — Gemini 3.1 Flash TTSプレビューが追加。低コストで表情豊か、制御しやすい音声合成ですMODEL — Gemini 3.5 Flashは一般提供済み。3.1 Proをほぼ全ベンチで上回りつつ4倍高速に動作しますSEARCH — File Searchはマルチモーダル検索に対応。gemini-embedding-2で画像をネイティブに埋め込み・検索できますDEPRECATION — 画像プレビューモデル2種が本日6/25に停止。利用中の自動化・スクリプトは本日中の移行が必須ですGA — 入れ替わりでgemini-3.1-flash-imageとgemini-3-pro-imageがネイティブ画像モデルの正式版になりましたMEDIA — 動画→画像生成に対応。動画を文脈として渡し高品質なサムネイル等を生成できます(3.1 flash image限定)AUDIO — Gemini 3.1 Flash TTSプレビューが追加。低コストで表情豊か、制御しやすい音声合成ですMODEL — Gemini 3.5 Flashは一般提供済み。3.1 Proをほぼ全ベンチで上回りつつ4倍高速に動作しますSEARCH — File Searchはマルチモーダル検索に対応。gemini-embedding-2で画像をネイティブに埋め込み・検索できます
記事一覧/API / SDK
API / SDK/2026-06-25上級

Gemini API の構造化出力が本番で静かにスキーマから外れるとき — 検証と再試行を計測する運用メモ

response_schema を指定しても、Gemini の構造化出力は本番で時折スキーマからずれます。失敗を握りつぶさず計測し、finish_reason で原因を切り分け、エラーを差し戻して再試行する。実運用で安定させた検証パイプラインのメモです。

gemini-api250structured-output17pydanticvalidation3production93reliability3

プレミアム記事

response_schema を指定したのに、本番のログに ValidationError が点々と残る。再現させようとローカルで同じプロンプトを何十回叩いても、きれいに通る。けれど数千リクエストに一度くらい、ratingint のはずなのに "9点" という文字列で返り、後段の集計が落ちている——。

構造化出力は「ほぼ守られる」けれど「必ず守られる」わけではありません。やっかいなのは、この逸脱が静かに起きることです。例外で派手に落ちてくれれば気づけますが、try/except で握りつぶしていると、失敗が null や既定値にすり替わって、データだけが少しずつ濁っていきます。

ここでは、構造化出力を本番で安定させるために私が落ち着いた考え方を、動くコードとともに整理します。要点は3つです。失敗を例外ではなく「失敗率」として計測すること。原因を finish_reason で切り分けること。そして、ただ再試行するのではなく、エラーをモデルに差し戻して直させること。公式ドキュメントが「対応しています」と書く機能を、運用に耐える形にするまでの距離を埋めるメモだと思ってください。

「動くコード」と「落ちないコード」は別物

まず、基本形は素直です。Pydantic v2 のモデルを response_schema に渡し、返ってきた JSON を model_validate_json でパースします。

import os
from pydantic import BaseModel, Field
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
 
class ReviewSummary(BaseModel):
    product_name: str = Field(description="製品名")
    rating: int = Field(description="1〜5の整数評価", ge=1, le=5)
    summary: str = Field(description="120文字以内の要約")
 
resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="このレビューを要約してください: ……",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=ReviewSummary,
    ),
)
review = ReviewSummary.model_validate_json(resp.text)

これはデモとしては完璧で、社内でも「ちゃんと型で返ってくる」と評判が良いはずです。問題は、このコードが「正常系しか想定していない」ことにあります。resp.textNone になる経路、スキーマからわずかにずれた JSON が返る経路、安全フィルターが途中で止める経路——どれも本番では現実に起きます。デモで一度動いたことと、無人で何万回も回って落ちないことの間には、はっきりした距離があります。

失敗を例外ではなく「率」で持つ

最初にやるべきは、ハンドリングの精緻化ではなく計測です。構造化出力の失敗率がそもそも 0.1% なのか 5% なのかで、打つ手はまったく変わります。私はまず、成功・失敗を素通りで記録するだけの薄い計装を入れます。

from dataclasses import dataclass, field
from collections import Counter
import time
 
@dataclass
class StructuredOutputMetrics:
    total: int = 0
    success: int = 0
    failures: Counter = field(default_factory=Counter)  # 原因別
 
    def record_success(self):
        self.total += 1
        self.success += 1
 
    def record_failure(self, reason: str):
        self.total += 1
        self.failures[reason] += 1
 
    @property
    def failure_rate(self) -> float:
        return 0.0 if self.total == 0 else 1 - self.success / self.total
 
    def report(self) -> str:
        top = ", ".join(f"{k}={v}" for k, v in self.failures.most_common(5))
        return f"rate={self.failure_rate:.3%} n={self.total} [{top}]"
 
METRICS = StructuredOutputMetrics()

ポイントは、失敗を一括りにせず原因別の Counter で持つことです。「finish_reasonMAX_TOKENS で切れた」のと「JSON は完全だが rating が範囲外だった」のとでは、まったく別の対処になります。これを混ぜて「失敗 3%」とだけ記録すると、どこを直せばいいのか永遠に分かりません。私は1時間ごとにこの report() をログへ吐き、failure_rate が普段の3倍を超えたら通知が飛ぶようにしています。モデルのバージョンが切り替わった日や、プロンプトをいじった直後に、この数字が静かに跳ねることがあるからです。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
構造化出力の失敗を「例外」ではなく「失敗率」として計測し、しきい値で警告を出す計装の入れ方
finish_reason・空テキスト・スキーマ逸脱を切り分け、原因別にリトライ方針を変える分岐の実装
エラー内容をプロンプトに差し戻す『修正付き再試行』と、Union/strict/深いネストで踏んだ地雷の回避策
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

API / SDK2026-06-25
Gemini API × TypeScript 型安全AIアプリケーション設計ガイド — Zodスキーマ・Structured Output・ストリーミングの統合アーキテクチャ
Gemini APIとTypeScriptで型安全なAIアプリケーションを構築する方法を解説。Zodバリデーション、Structured Output、ストリーミング、エラーハンドリングを統合した本番アーキテクチャを実装します。
API / SDK2026-06-13
Gemini の生成は通ったのに公開が落ちた朝 — 高価な出力を取りこぼさない『生成アウトボックス』の設計
生成は成功したのに、公開処理の直前でプロセスが落ちる。すると高価な出力が消え、もう一度同じ生成に課金することになります。生成結果を先に永続化し、公開を冪等な後続処理に分離する『生成アウトボックス』の実装と、6月の障害下での運用記録をまとめました。
API / SDK2026-05-23
Gemini API の Structured Output でバリデーションエラーが返るときの原因と対処
Gemini API の responseSchema を使った構造化出力で頻発するバリデーションエラー(INVALID_ARGUMENT、schema mismatch、空配列、不要フィールド)の原因と、実運用で安定させるための実装パターンを個人開発の現場視点でまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →