GEMINI LABEN
AGENT — Managed AgentsがGemini APIで公開プレビュー入り。分離されたGoogleホストのLinuxサンドボックスで自律・ステートフルなエージェントを動かせますSEARCH — File Searchがマルチモーダル対応となり、gemini-embedding-2(GA)で画像をネイティブに埋め込み・検索できますTTS — gemini-3.1-flash-tts-previewが音声生成のストリーミングに対応し、逐次再生できますMODEL — Gemini 3.5 FlashがGA(正式版)となり、gemini-flash-latestの実体になりましたDEPRECATION — 画像生成モデルは2026年8月17日に提供終了予定です。依存パイプラインは移行先の確認をENTERPRISE — Gemini 3.5 FlashがGlobal・US・EUのGemini Enterpriseで一般提供されましたAGENT — Managed AgentsがGemini APIで公開プレビュー入り。分離されたGoogleホストのLinuxサンドボックスで自律・ステートフルなエージェントを動かせますSEARCH — File Searchがマルチモーダル対応となり、gemini-embedding-2(GA)で画像をネイティブに埋め込み・検索できますTTS — gemini-3.1-flash-tts-previewが音声生成のストリーミングに対応し、逐次再生できますMODEL — Gemini 3.5 FlashがGA(正式版)となり、gemini-flash-latestの実体になりましたDEPRECATION — 画像生成モデルは2026年8月17日に提供終了予定です。依存パイプラインは移行先の確認をENTERPRISE — Gemini 3.5 FlashがGlobal・US・EUのGemini Enterpriseで一般提供されました
記事一覧/高度な活用
高度な活用/2026-07-07上級

構造化出力のストリーミングで、完成したフィールドから順に画面へ出す — 不完全なJSONを安全に読む部分パース

responseSchema でストリーミングすると、JSONが閉じるまで画面は真っ白なまま待たされます。不完全なJSONを安全に補完して読む部分パースと、一度出した値を後退させない逆流防止を組み、最初のフィールドまでの体感を実測とともに縮める設計を残します。

gemini-api269ストリーミング10構造化出力11個人開発77本番運用43

プレミアム記事

AdMob の日次レポートを要約するカードを、自分の管理画面に足したときのことです。Gemini に数値を渡し、responseSchema で「見出し・要点・推奨アクション・スコア」を返してもらう、よくある構成でした。

動きはしました。ただ、押してから結果が出るまで、画面は数秒間まっさらのままでした。裏ではトークンが届いているのに、JSONが最後の } で閉じるまで JSON.parse が投げ続けるので、何も描けなかったのです。

ストリーミングにしているのに、体感は一括取得と変わりませんでした。むしろ「押したのに無反応」に見える分だけ、印象は悪くなっていました。

その真っ白な数秒を、完成したフィールドから順に埋めていく数百ミリ秒へ。個人開発の自分の画面で実際にそう変えられた、部分パースと逆流防止の設計を、私自身のつまずきも含めて動くコードとともに残します。

完成待ちが体感を悪くする理由

構造化出力のストリーミングでは、モデルは JSON を頭から少しずつ吐き出します。届くのはこういう断片の連なりです。

到着段階バッファの中身(イメージ)JSON.parse
序盤{"title":"省エネ壁紙失敗
中盤{"title":"省エネ壁紙12選","summary":"暗色失敗
終盤{...,"score":87}成功

標準の JSON.parse は、閉じていない文字列やオブジェクトを一切許しません。ですから終盤で全体が閉じるまで、私たちは何も取り出せません。

けれど中身をよく見ると、title は序盤の時点で既に確定しています。読者に見せられる情報が手元にあるのに、末尾が閉じていないという一点で握りつぶしている。ここが体感の損失点でした。

やりたいことは単純です。バッファが届くたびに、その時点で確定している先頭のフィールドだけを安全に取り出して、順番に画面へ流す。これだけです。

不完全なJSONを安全に補完して読む

方針は、バッファをそのままパースして失敗したら、閉じられる最小限の補完をしてからもう一度パースする、というものです。開いたままの文字列・オブジェクト・配列を数えて、足りない分の閉じ括弧を足します。

半端に書かれている末尾の値(打ちかけの数値やキーだけの箇所)は、揺れの原因になるので落とします。完成した部分だけを見せるのが目的だからです。

// 開いたままの構造を閉じ、半端な末尾を落として「今わかる範囲」を作る
function completePartial(buf: string): string {
  const stack: string[] = [];
  let inStr = false;
  let esc = false;
  let out = buf;
 
  for (let i = 0; i < buf.length; i++) {
    const c = buf[i];
    if (inStr) {
      if (esc) esc = false;
      else if (c === "\\") esc = true;
      else if (c === '"') inStr = false;
      continue;
    }
    if (c === '"') inStr = true;
    else if (c === "{" || c === "[") stack.push(c);
    else if (c === "}" || c === "]") stack.pop();
  }
 
  if (inStr) out += '"';              // 開いたままの文字列を閉じる
  out = out
    .replace(/,\s*$/, "")             // 末尾のカンマを落とす
    .replace(/:\s*$/, ": null")       // 値のないキーは null で埋める
    .replace(/([0-9])\.$/, "$1");     // 打ちかけの小数点を切る
 
  for (let i = stack.length - 1; i >= 0; i--) {
    out += stack[i] === "{" ? "}" : "]";
  }
  return out;
}
 
export function safeParse(buf: string): unknown | null {
  try { return JSON.parse(buf); } catch { /* まだ閉じていない */ }
  try { return JSON.parse(completePartial(buf)); } catch { return null; }
}

completePartial の肝は、文字列の内側を数えないことです。"{" のような括弧が値の文字列に含まれていても、inStr フラグで無視します。これを忘れると、本文に括弧が入った瞬間に補完が壊れます。

これを Gemini のストリームに繋ぎます。チャンクが届くたびにバッファへ足し、safeParse で「今わかる範囲」を取り出します。

import { GoogleGenAI } from "@google/genai";
 
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
 
async function streamCard(prompt: string, onField: (k: string, v: unknown) => void) {
  const stream = await ai.models.generateContentStream({
    model: "gemini-flash-latest",
    contents: prompt,
    config: {
      responseMimeType: "application/json",
      responseSchema: {
        type: "object",
        properties: {
          title:   { type: "string" },
          summary: { type: "string" },
          actions: { type: "array", items: { type: "string" } },
          score:   { type: "integer" },
        },
        propertyOrdering: ["title", "summary", "actions", "score"],
        required: ["title", "summary", "actions", "score"],
      },
    },
  });
 
  let buf = "";
  const emitter = fencedEmitter(onField);
  for await (const chunk of stream) {
    buf += chunk.text ?? "";
    const obj = safeParse(buf);
    if (obj && typeof obj === "object") emitter(obj as Record<string, unknown>);
  }
}

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

この記事の続きを読む

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

この記事で得られること
閉じていないJSONの末尾を安全に補完して逐次パースする関数を、そのまま動くTypeScriptとして持ち帰れます
一度表示したフィールドが後続チャンクで消えたり揺れたりする逆流を、フェンシングで止める実装を手に入れられます
propertyOrdering で到着順を設計し、最初のフィールドまでの時間を実測で約2.4秒から0.4秒へ縮めた経緯を追体験できます
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

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

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

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

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

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

関連記事

高度な活用2026-07-07
モデルの提供終了通知が届くたびに慌てないために — Gemini 依存を一枚の層に閉じ込める個人アプリ設計
画像生成モデルの停止通知が届いた朝、依存が散らばっていると差し替えは半日仕事になります。Gemini 依存を一枚のポート層に閉じ込め、フォールバックと廃止期限のCI監視まで含めた個人アプリ設計を、動くコードとともに残します。
開発ツール2026-06-02
Bun と Hono で Gemini API の軽量バックエンドを組む — 個人開発の小さなツールを自分の手に取り戻す実装メモ
Node と Express で書いた Gemini バックエンドが、依存関係とビルド時間で重くなっていませんか。Bun と Hono に載せ替えて、ストリーミング・レート制限・コスト上限・セルフホストまでを一つの軽い実行環境にまとめた実装と判断軸を、現場で踏んだ落とし穴とあわせて整理します。
高度な活用2026-05-10
Gemini 3.2 Pro × Imagen 4 で自作アートを壁紙 120 種類に展開した 30 日 — アーティスト個人開発者の素材供給パイプライン
自分のアート作品 1 枚を起点に、Gemini 3.2 Pro で構造解析・Imagen 4 で派生展開して壁紙シリーズ 120 枚を 30 日で揃えた本番パイプラインの実装記録。コスト・品質ゲート・アプリ側 KPI まで具体数値で公開します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →