GEMINI LABEN
NANOLITE — Nano Banana 2 Liteが登場しました。Googleで最も速く、最もコスト効率の高いGemini Imageモデルで、軽量な画像生成を安く回したい用途に向いていますOMNIFLASH — Gemini Omni Flashがpublic previewになりました。ネイティブにマルチモーダルなモデルで、企業や開発者が独自の動的な動画ワークフローを構築できますAGENTS — Managed Agentsが拡張されました。background: trueでサーバー側の非同期実行とポーリング、リモートMCPサーバー連携、対話をまたぐ認証情報のリフレッシュに対応しますMEMORY — Memory BankのIngestEvents APIが一般提供になりました。イベントの取り込みとメモリ生成を分離し、コンテンツを継続的にストリームできますTHROUGHPUT — Provisioned Throughputで、同一モデル・同一リージョンに対して最大7件の保留オーダーを提出できるようになりましたDEPRECATE — 画像生成モデルは8月17日に、Gemini Enterprise Agent PlatformのGrok 4.1系は8月20日に停止される予定ですNANOLITE — Nano Banana 2 Liteが登場しました。Googleで最も速く、最もコスト効率の高いGemini Imageモデルで、軽量な画像生成を安く回したい用途に向いていますOMNIFLASH — Gemini Omni Flashがpublic previewになりました。ネイティブにマルチモーダルなモデルで、企業や開発者が独自の動的な動画ワークフローを構築できますAGENTS — Managed Agentsが拡張されました。background: trueでサーバー側の非同期実行とポーリング、リモートMCPサーバー連携、対話をまたぐ認証情報のリフレッシュに対応しますMEMORY — Memory BankのIngestEvents APIが一般提供になりました。イベントの取り込みとメモリ生成を分離し、コンテンツを継続的にストリームできますTHROUGHPUT — Provisioned Throughputで、同一モデル・同一リージョンに対して最大7件の保留オーダーを提出できるようになりましたDEPRECATE — 画像生成モデルは8月17日に、Gemini Enterprise Agent PlatformのGrok 4.1系は8月20日に停止される予定です
記事一覧/API / SDK
API / SDK/2026-04-26上級

Gemini API のレスポンスを Schema 検証で守る — 想定外フォーマットを本番で漏らさない設計

Gemini API の構造化出力は便利ですが、本番運用で「想定外フォーマット」が混じる瞬間は必ず来ます。Zod / Pydantic を組み合わせた多層検証、失敗時の縮退戦略、再試行と修復プロンプトの設計まで、個人開発で運用している防御線をまとめます。

Gemini API191構造化出力12Schema 検証ZodPydantic本番運用47

Gemini API に構造化出力(Structured Output / responseSchema)を使い始めて、しばらくは「これでもう JSON.parse の例外を恐れる必要はない」と思っていました。実際、開発中はほぼ 100% 期待通りのスキーマで返ってきます。ところが本番に出して数週間後、ある日突然「JSON 構造は正しいのに、enum 値が見たことのない文字列になっている」というエラーが立て続けに発生しました。

調べてみると、Gemini の構造化出力は「JSON として valid な出力を出す」ことは保証してくれますが、「スキーマで宣言した制約を完全に守る」ことは保証していません。enum、min/max、required、pattern といった制約は、モデル側のベストエフォートで満たされる、というのが実情です。本番で大量に動かすと、必ずどこかで漏れます。

私が運用している「Gemini API のレスポンスを Schema 検証で守る」ための多層防御の設計を実例とともに整理しました。Zod / Pydantic を使った検証、失敗時の縮退戦略、再試行と修復プロンプトの設計まで、コードレベルで残します。

構造化出力モードでも壊れる 3 つの典型パターン

私が観測した「想定外フォーマットが本番で漏れる」事例を分類すると、次の 3 パターンに収まります。

ひとつ目は enum 値の崩れです。["positive", "negative", "neutral"] を期待していたフィールドに、"mixed""slightly positive" が入ってきます。日本語で出力させていると "ポジティブ" のように、英語側の enum 値が翻訳された値が紛れることもあります。プロンプトで「enum の値以外を返さないでください」と書いても、3% 程度の頻度で漏れます。

ふたつ目は 数値範囲の逸脱です。{ "score": { "type": "number", "minimum": 0, "maximum": 100 } } と宣言していても、稀に 105 や -3 が返ってきます。私の手元のデータでは、感情分析タスクで 0.5% 程度の頻度で範囲外が出ます。レアですが本番に流すと月に数件は当たります。

みっつ目は 配列要素数の崩れです。「上位 3 件のキーワードを返す」と宣言しても、稀に 2 件、稀に 5 件返ります。フロント側で「3 件決め打ち」のレイアウトを組んでいると UI が崩れます。

これらは Gemini 側の不具合というより、現在の構造化出力 API の本質的な制約です。本番運用するなら、API のレスポンスを「契約として信頼する」のではなく「契約に違反していないかをこちら側で検証する」設計に切り替える必要があります。

多層検証の全体像 — 3 段の防御線

私が運用している多層検証は、次の 3 段構成です。

  1. 第 1 段: スキーマ検証(Zod / Pydantic)
    • レスポンスが宣言したスキーマに完全準拠しているかを判定する
  2. 第 2 段: 修復プロンプトによる再試行
    • 第 1 段で失敗したとき、エラー内容を含めて Gemini に再生成させる
  3. 第 3 段: 縮退ハンドラ
    • 第 2 段でも失敗したとき、空値や近似値で UI を壊さないように吸収する

それぞれの段を順に見ていきます。

第 1 段: スキーマ検証 — Zod / Pydantic で型を確定する

Gemini の構造化出力で responseSchema を渡しても、こちら側でも同じ意味のスキーマを別途定義します。これは二重管理に見えますが、本番防御の観点では必須です。

TypeScript なら Zod、Python なら Pydantic を使います。私の運用例(TypeScript / Zod)はこんな形です。

import { z } from "zod";
 
const SentimentEnum = z.enum(["positive", "negative", "neutral"]);
 
const AnalysisResultSchema = z.object({
  sentiment: SentimentEnum,
  score: z.number().min(0).max(100),
  keywords: z.array(z.string()).length(3),
  summary: z.string().min(10).max(500),
});
 
type AnalysisResult = z.infer<typeof AnalysisResultSchema>;
 
function parseGeminiResponse(rawJson: string): AnalysisResult {
  const parsed = JSON.parse(rawJson);
  return AnalysisResultSchema.parse(parsed);
}

ここで重要なのは、Gemini に渡す responseSchema と、Zod スキーマが同一の意味を持つように同期させることです。私はこれを 1 つの定義から両方を生成する形にしています。Zod スキーマから zod-to-json-schema で JSON Schema を出力し、それを Gemini に渡しています。

import { zodToJsonSchema } from "zod-to-json-schema";
 
const responseSchemaForGemini = zodToJsonSchema(AnalysisResultSchema, {
  target: "openApi3",
});
 
const result = await model.generateContent({
  contents: [{ parts: [{ text: prompt }] }],
  generationConfig: {
    responseMimeType: "application/json",
    responseSchema: responseSchemaForGemini as any,
  },
});

二重定義しないことで、スキーマの意味のずれが起きません。Pydantic を使う場合も同様に model_json_schema() で JSON Schema を出力できます。

第 2 段: 修復プロンプトによる再試行

第 1 段で ZodError が出たとき、即座に縮退に落とすのではなく、Gemini にエラー内容を教えて再生成させます。これが「修復プロンプト」と呼んでいる仕組みです。

async function generateWithRepair(
  prompt: string,
  maxAttempts: number = 2,
): Promise<AnalysisResult> {
  let lastError: z.ZodError | null = null;
  let lastRawResponse: string | null = null;
 
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const finalPrompt = lastError
      ? buildRepairPrompt(prompt, lastRawResponse!, lastError)
      : prompt;
 
    const raw = await callGemini(finalPrompt);
 
    try {
      return parseGeminiResponse(raw);
    } catch (err) {
      if (err instanceof z.ZodError) {
        lastError = err;
        lastRawResponse = raw;
        continue;
      }
      throw err;
    }
  }
 
  throw new SchemaValidationFailed(lastError!, lastRawResponse!);
}
 
function buildRepairPrompt(
  originalPrompt: string,
  lastResponse: string,
  error: z.ZodError,
): string {
  const issues = error.issues
    .map((i) => `- ${i.path.join(".")}: ${i.message}`)
    .join("\n");
 
  return `${originalPrompt}
 
前回の応答は次の通りでしたが、スキーマ違反が含まれていました:
 
\`\`\`json
${lastResponse}
\`\`\`
 
次の問題を修正して、もう一度同じスキーマに従って出力してください:
 
${issues}
`;
}

修復プロンプトの効果は私の運用で見えていて、第 1 段で 3% 失敗していたものが、第 2 段の 1 回再試行で 0.3% まで下がります。さらに 2 回目の再試行で 0.05% 以下になります。コストは増えますが、サンプリング率を見ると失敗時のみのコスト増なので、月単位でみても数 % 程度の増加で済んでいます。

注意点として、修復プロンプトの中に「前回の応答」を載せるとコンテキスト長が膨らみます。私の運用では、再試行は最大 2 回までに制限し、3 回目で第 3 段の縮退に落とします。

第 3 段: 縮退ハンドラ — UI を壊さない最後の防御

第 2 段でも失敗するレスポンスは、月に数件のオーダーで必ず出ます。ここで例外を投げてしまうと、ユーザーには「処理に失敗しました」というエラー画面しか見せられません。これは UX として良くありません。

縮退ハンドラは、スキーマ違反を許容できる形に吸収して、UI が破綻しないようにします。私の運用ではフィールドごとに縮退戦略を決めています。

function fallbackForSchemaFailure(
  raw: unknown,
  error: z.ZodError,
): AnalysisResult {
  const partial = raw as Partial<AnalysisResult>;
 
  return {
    sentiment: SentimentEnum.safeParse(partial.sentiment).success
      ? (partial.sentiment as "positive" | "negative" | "neutral")
      : "neutral",
    score: clamp(typeof partial.score === "number" ? partial.score : 50, 0, 100),
    keywords: Array.isArray(partial.keywords)
      ? partial.keywords.slice(0, 3).concat(Array(3).fill("")).slice(0, 3)
      : ["", "", ""],
    summary: typeof partial.summary === "string"
      ? partial.summary.slice(0, 500).padEnd(10, " ")
      : "(要約を生成できませんでした)",
  };
}

縮退戦略のポイントは、**「壊れた値を捨てて中央値で埋める」のではなく、「使える値はできる限り使う」**ことです。例えば sentiment が "slightly positive" なら、第 1 段では落としますが、縮退時には "positive" に丸めて使います。score が 105 なら 100 にクリップします。

ただし、縮退で吸収したことは必ずログに残します。これがないと「気づいたら全部のレスポンスが縮退で動いていた」という事故が起きます。

logger.warn("schema_fallback_used", {
  task_id: taskId,
  zod_issues: error.issues.map((i) => ({
    path: i.path.join("."),
    code: i.code,
  })),
});

このログを Honeycomb / Datadog に流して、縮退率が 1% を超えたらアラートを上げる、という運用にしています。

スキーマ管理ルール — 二重定義しないために

3 段防御を運用していると、スキーマ変更のたびに「Zod / Pydantic 側」「Gemini に渡す responseSchema 側」「縮退ハンドラ側」の 3 箇所を同期する必要が出てきます。これを手作業でやるとずれます。

私が運用しているルールは次の通りです。

第一に、Zod / Pydantic スキーマを唯一の真実とする。Gemini に渡す JSON Schema は必ず生成、縮退ハンドラはスキーマからフィールド一覧を取り出すヘルパで生成します。

第二に、スキーマファイルを src/schemas/gemini/ に集約する。1 ファイル 1 スキーマで管理し、ファイル名はタスク名と一致させる。sentiment_analysis.tskeyword_extraction.ts のように。

第三に、スキーマ変更時に旧スキーマでのレスポンスを壊さない。スキーマを後方互換的に拡張するためのルールを決めておく。新規フィールドは optional から始める、enum は値を増やすだけにして消さない、など。

このルールを守っていると、3 段防御の保守コストが大きく下がります。

運用 1 ヶ月で見えた数字 — 私の実例

3 段防御を運用 1 ヶ月で、私のリポジトリではこんな数字が見えています。参考までに残しておきます。

  • 第 1 段スキーマ検証の失敗率: 約 2.8%
  • 第 2 段修復プロンプトでの回復率: 89%(残り 11% が第 3 段に落ちる)
  • 第 3 段縮退ハンドラの発動率: 約 0.3%
  • 本番でユーザーに「エラー画面」を出した件数: 0 件

第 3 段が 0.3% 発動しているということは、月数千リクエストの規模だと月数十件は縮退で吸収しています。これらが全部スキーマ違反として例外になっていたら、ユーザー体験は確実に悪化していました。

この多層防御は最初の構築に半日〜1 日かかります。ですが構築後は、Gemini API のスキーマ変更や、新規タスクへの応用が定型化できます。「Gemini に構造化出力を出させて、たまに壊れる」という不安定さに悩んでいる方は、まず Zod / Pydantic スキーマを 1 つ書いて、第 1 段だけ実装するところから始めてみてください。

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API / SDK2026-04-30
Gemini API の構造化出力を本番品質まで引き上げる設計図
Gemini API の構造化出力(Structured Output)を本番アプリケーションで安定稼働させるための実践的な設計図。スキーマ設計・エラー対応・パフォーマンス最適化まで体系化します。
API / SDK2026-06-28
Gemini × Qdrant のハイブリッド検索が再現率を静かに落としていたとき — RRF の重みと疎ベクトルのズレを計測する運用メモ
Gemini の埋め込みと Qdrant のハイブリッド検索を本番で回すと、ダッシュボードは緑のまま再現率だけが静かに落ちます。RRF の重み・疎ベクトルのズレ・ペイロードインデックスの欠落を計測で捕まえ、品質バジェットで守る運用メモです。
API / SDK2026-06-27
Gemini の構造化出力でJSONのフィールド順がブレる — propertyOrdering で固定する実装メモ
responseSchema で型を縛っているのに、出力JSONのキーの並びが呼び出しごとに変わる。スナップショットテストが理由なく赤くなる原因はこれでした。propertyOrdering でフィールド順を固定し、few-shot の順序も揃え、差分ノイズを消すまでをコード付きでまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →