GEMINI LABEN
SUNSET — 画像生成モデルの停止まで残り6日です。imagen-4.0 系と Gemini 3 Image 系が8月17日に止まりますMIGRATE — 移行先として gemini-3.1-flash-image が案内されています。generate_images から generate_content への書き換えが必要ですCHECK — 移行後に同じプロンプトで同じ絵が出る保証はありません。必要な生成物は停止前に確保しておく判断が要りますCLASSROOM — 8月17日は Gemini in Classroom のモバイル提供が始まる日でもあります。web は8月10日に全年齢の学生へ広がりましたDEPRECATION — Grok 4.1 ファミリーの停止は8月20日、gemini-robotics-er-1.6-preview は8月31日で、後継は er-2 系ですCHANGELOG — Gemini API の changelog は7月30日が最新のままです。直近の大きな変更は Gemini 3.6 Flash と 3.5 Flash-Lite の GA ですSUNSET — 画像生成モデルの停止まで残り6日です。imagen-4.0 系と Gemini 3 Image 系が8月17日に止まりますMIGRATE — 移行先として gemini-3.1-flash-image が案内されています。generate_images から generate_content への書き換えが必要ですCHECK — 移行後に同じプロンプトで同じ絵が出る保証はありません。必要な生成物は停止前に確保しておく判断が要りますCLASSROOM — 8月17日は Gemini in Classroom のモバイル提供が始まる日でもあります。web は8月10日に全年齢の学生へ広がりましたDEPRECATION — Grok 4.1 ファミリーの停止は8月20日、gemini-robotics-er-1.6-preview は8月31日で、後継は er-2 系ですCHANGELOG — Gemini API の changelog は7月30日が最新のままです。直近の大きな変更は Gemini 3.6 Flash と 3.5 Flash-Lite の GA です
記事一覧/API / SDK
API / SDK/2026-05-20上級

Gemini API 構造化出力スキーマを本番アプリで安全に進化させる設計記録

個人開発のアプリ事業でGemini APIのJSON出力スキーマを変えるとき、何を契約として守り、何を段階的に切り替えたか。Dual-EmitパターンとSunsetプロトコルの実装記録です。

gemini-api281structured-output21schema-evolutionproduction105indie-development4

プレミアム記事

1ヶ月ほど前、私が個人で運営している壁紙アプリの一つで、Gemini API を使ったテーマ別レコメンド機能が早朝5時頃から突然エラーになり始めました。Crashlytics の通知で起きたとき、症状を見て胃がきゅっとしました。クラッシュ率がじわじわ伸びていて、エラーの中身は Type 'undefined' is not assignable to type 'string' — クライアント側の Zod バリデーション失敗でした。

原因は単純でした。前日に Google AI Studio で response_schema の optional フィールドを一つ追加していて、Gemini が新しいフィールドを生成するついでに、別の optional フィールドの「未設定時の挙動」が undefined から空文字に変わっていたのです。文書化されていない挙動の変化で、運用中のアプリ群のうち、その日アクセスしてきたユーザーの約2割が影響を受けました。AdMob の eCPM が安定していた中で、レコメンド機能経由のセッション継続率が一晩で 12% 落ち、月収換算で 18万円相当のインプレッションが消える計算になりました。

このとき身に染みて理解したのは、Gemini API が返す JSON は単なるデータ構造ではなく、クライアントと結ぶ「契約」だということです。それも、サードパーティのモデル更新で勝手に書き換えられかねない、かなり脆い契約です。今回まとめておきたいのは、その事故をきっかけに私が組み直した「スキーマを契約として扱う」設計と、2026年4月から実運用している Dual-Emit パターン・Sunset プロトコル・compatibility checker の実装記録です。

壁紙アプリで起きた JSON 契約破綻 — 数値で振り返る

事故の輪郭を数字で示しておきます。私の個人開発アプリは壁紙系・癒し系・引き寄せ系を中心に複数本を運用しており、Gemini API は2026年初頭からレコメンド・タグ生成・短い導入文の自動生成に組み込んでいました。

事故当日の影響範囲は次の通りでした。

  • 影響を受けたアプリ: 18本中6本(Gemini レコメンドを使っているもの)
  • セッション継続率の下落: 平均 12.4%、最悪のアプリで 18.7%
  • AdMob インプレッション減: 24時間で約 36,500 インプレッション
  • 月収換算の損失: 18万円相当(eCPM 5.20 USD ベースで概算)
  • 完全復旧までの所要時間: コード修正は1時間、ストア審査と段階配信を含めると約 23時間

経緯を振り返ると、特に重い問題は事故そのものより、原因究明にかかった時間でした。クライアント側の Zod バリデーション失敗ログだけが手元にあって、「Gemini が何を返したか」の生レスポンスがほぼ残っていなかったのです。Cloudflare Workers 側のログは7日分しか保持しておらず、しかも構造化出力の body は省略する設定になっていました。事故の根本対策の半分は、この観測性の欠如への対処でした。

スキーマを「契約」として扱う設計 — 内部 IR と外部公開層を分離する

事故後にまず変えたのは、「Gemini からのレスポンスをそのまま使う」のをやめて、内部表現(Internal Representation, IR)と外部公開スキーマを2層に分けたことです。

それまでの設計は単純で、Gemini が返した JSON を Zod でパースして即クライアントに返していました。これだとモデル側の挙動が変わるたびに契約も勝手に変わってしまいます。新しい設計では「Gemini から受け取ったものをまず内部 IR に正規化し、そこから別の Zod スキーマで外部公開形式に整形する」という流れに統一しました。

// schemas/recommendation.ts
import { z } from "zod";
 
// 入口スキーマ: Gemini が返しうるブレを吸収する
export const GeminiRawRecommendation = z.object({
  theme_tag: z.string().min(1).max(64),
  // optional でも null / undefined / 空文字すべて許容して内部で正規化
  subtitle: z.string().optional().nullable().transform((v) => v?.trim() || null),
  confidence: z.number().min(0).max(1).default(0.5),
  // モデルが余計なフィールドを返しても拒否しない
}).passthrough();
 
// 内部 IR: アプリ内部の不変条件を守る正規化済みの形
export const Recommendation = z.object({
  theme: z.string().min(1).max(64),
  subtitle: z.string().nullable(),
  confidence: z.number().min(0).max(1),
  source: z.enum(["gemini-3.2", "gemini-3.1-fallback", "cached"]),
});
 
// 外部公開: クライアントが受け取る安定した契約。バージョンを切る
export const RecommendationV2 = z.object({
  schemaVersion: z.literal("v2"),
  theme: z.string(),
  subtitle: z.string().default(""), // クライアントは undefined を受け取らない保証
  confidence: z.number(),
});

ポイントは3つあります。

  1. 入口スキーマでは passthrough() を使って未知のフィールドを拒否せず、optional は nullable も同時に許容します。モデル側の挙動変動を入口で吸収するためです
  2. 内部 IR で subtitle: z.string().nullable() のように「未設定なら null」と決め切る。undefined と空文字を内部に持ち込まない判断は、私の経験では一番効きました
  3. 外部公開スキーマは schemaVersion リテラル付きで切る。クライアントは schemaVersion を見てパーサを選びます

undefined を内部に流さない、という一手間が後でいちばん効きます。境界面で形を整えてしまえば、内側のコードはずっと働きやすくなります。

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

この記事の続きを読む

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

この記事で得られること
Zodベースの2層スキーマ設計(入口IR・内部IR・外部公開層)と Cloudflare Workers での実装コード
Dual-Emit パターンで Day 0〜Day 14 を段階移行した2週間ログと旧版稼働率の推移
サンプリングログとクライアントコードを突き合わせて破壊的変更を事前検出する Python スクリプト
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

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

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

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

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

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

関連記事

API / SDK2026-07-05
使っているモデルの廃止予告だけを拾う — url-context に公式チェンジログを読ませる仕組み
画像モデルの停止を公開3日前に知って肝を冷やしました。url-context に公式チェンジログを直接読ませ、自分が実際に使っているモデルの廃止予告だけを構造化して差分通知する仕組みを、そのまま動く Python と運用で削った過検知の調整まで含めて組み立てます。
API / SDK2026-06-25
Gemini API × TypeScript 型安全AIアプリケーション設計ガイド — Zodスキーマ・Structured Output・ストリーミングの統合アーキテクチャ
Gemini APIとTypeScriptで型安全なAIアプリケーションを構築する方法を解説。Zodバリデーション、Structured Output、ストリーミング、エラーハンドリングを統合した本番アーキテクチャを実装します。
API / SDK2026-06-25
Gemini API の構造化出力が本番で静かにスキーマから外れるとき — 検証と再試行を計測する運用メモ
response_schema を指定しても、Gemini の構造化出力は本番で時折スキーマからずれます。失敗を握りつぶさず計測し、finish_reason で原因を切り分け、エラーを差し戻して再試行する。実運用で安定させた検証パイプラインのメモです。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →