メインコンテンツまでスキップ

第05章:JSONで返してもらう(構造化の入口)🧾🔎

この章はひとことで言うと、AIの返事を“文章”じゃなくて“データ”として受け取れるようにする回です😆✨ (UIに出す・Firestoreに保存する・あとで集計する…が一気にラクになります!)


この章でできるようになること 🎯

  • 日報テキストから 「カテゴリ」「重要度」「ToDo配列」JSONで 抽出できる🧠
  • responseMimeType + responseSchema で、返事の形をだいたい固定できる🔧 (Firebase)
  • JSONが壊れても 復旧できる3段構え を用意できる🧯

Chaos vs Order

読む 📚✨:なぜJSONが強いの?(“文章”→“構造”)🧠

AIの返事がふつうの文章だけだと、あとでこうなりがちです👇

  • 「え、ToDoってどこからどこまで?」😵
  • 「重要度って数字なの?言葉なの?」😵‍💫
  • 「UIにカード表示したいのに、毎回パース地獄」🌀

そこで “構造化出力(structured output)” を使って、最初からJSONで返してもらうのが超便利! Firebase AI Logicでも、JSONを含む構造化出力は responseMimeTyperesponseSchema でコントロールできます🧩 (Firebase)


手を動かす 🧑‍💻✨:日報 → JSON抽出 → UI表示(React)

ここからは「日報を整えるボタン」の中の “抽出パート” を作ります🛠️


Schema Blueprint

① まずJSONの形を決める🧩

今回はUIで扱いやすいように、こんな形にします👇

  • category: "progress" | "issue" | "plan"
  • importance: 数字(あとで1〜5に丸める)
  • todos: { title, due? } の配列(最大10件)

ポイントはこれ👇

  • キー名は英語のほうが、TypeScriptの型やDB保存で事故りにくい👍
  • 配列はmaxItemsで上限をつける(暴走防止)🧯 (Firebase)
  • 厳密な「1〜5」みたいな制約はアプリ側で検証(スキーマで表現できない/効かない場合がある)🧰 (Firebase)

Extraction Flow

responseMimeTyperesponseSchema をセットする🔧

WebのSDKは firebase/ai を使います(ドキュメントのサンプルもこの形です)🧩 (Firebase) responseMimeType: "application/json" を指定すると、**「JSONとして返してね」**が明確になります。(Firebase)

import { getAI, getGenerativeModel, GoogleAIBackend, Schema } from "firebase/ai";
import { initializeApp } from "firebase/app";

const firebaseApp = initializeApp({
// ここは既に第2〜4章で用意済み想定(config)
});

const ai = getAI(firebaseApp, { backend: new GoogleAIBackend() });

/**
* 日報から「カテゴリ・重要度・ToDo」を抽出するためのJSONスキーマ
*/
const dailyReportSchema = Schema.object({
properties: {
category: Schema.enumString({ enum: ["progress", "issue", "plan"] }),
importance: Schema.number(),
todos: Schema.array({
maxItems: 10,
items: Schema.object({
properties: {
title: Schema.string(),
due: Schema.string(), // "2026-02-20" みたいに来たら嬉しい、くらいの気持ち
},
optionalProperties: ["due"],
}),
}),
},
});

// 生成モデル(JSON固定)
export const dailyReportModel = getGenerativeModel(ai, {
model: "GEMINI_MODEL_NAME",
generationConfig: {
responseMimeType: "application/json",
responseSchema: dailyReportSchema,
},
});

スキーマで使える項目が限られていたり、基本は“全部必須扱い”になりやすいので、必要なら optionalProperties をちゃんと指定します🙂 (Firebase)


UI Mapping

③ React側:抽出してカード表示する📇✨

まずは「抽出→画面に見せる」まで最短でいきます🚀

import React, { useMemo, useState } from "react";
import { z } from "zod";
import { dailyReportModel } from "./ai";

// ✅ 受け取ったJSONを“最後に守る”ためのZod(保険)
const DailyReportZ = z.object({
category: z.enum(["progress", "issue", "plan"]),
importance: z.number(),
todos: z.array(
z.object({
title: z.string().min(1),
due: z.string().optional(),
})
),
});
type DailyReport = z.infer<typeof DailyReportZ>;

function clampImportance(n: number) {
// 重要度が 1〜5 に入らなくても、ここで救う🙏
if (!Number.isFinite(n)) return 3;
return Math.min(5, Math.max(1, Math.round(n)));
}

export default function DailyReportExtractor() {
const [input, setInput] = useState("");
const [data, setData] = useState<DailyReport | null>(null);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);

async function run() {
setBusy(true);
setError(null);
setData(null);

try {
const prompt = `
次の日報テキストから、カテゴリ(category)、重要度(importance)、ToDo一覧(todos)を抽出してください。
- 出力はJSONのみ
- todosは最大10件
- dueは分かれば "YYYY-MM-DD" 形式、分からなければ省略

日報:
${input}
`.trim();

const result = await dailyReportModel.generateContent(prompt);
const text = result.response.text(); // JSON文字列のはず
const json = JSON.parse(text);

const parsed = DailyReportZ.safeParse(json);
if (!parsed.success) {
throw new Error("JSONの形が想定と違いました(検証NG)");
}

// 仕上げ:importanceだけ丸める
const fixed: DailyReport = {
...parsed.data,
importance: clampImportance(parsed.data.importance),
todos: parsed.data.todos.slice(0, 10),
};

setData(fixed);
} catch (e: any) {
setError(e?.message ?? "エラーが起きました");
} finally {
setBusy(false);
}
}

return (
<div style={{ display: "grid", gap: 12, maxWidth: 720 }}>
<h2>日報 → JSON抽出 🧾✨</h2>

<textarea
rows={8}
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="今日やったこと、困ってること、明日やること…をラフに書く✍️"
/>

<button onClick={run} disabled={busy || input.trim().length === 0}>
{busy ? "抽出中…🤖" : "抽出する🧠"}
</button>

{error && <div style={{ color: "crimson" }}>⚠️ {error}</div>}

{data && (
<div style={{ border: "1px solid #ddd", padding: 12, borderRadius: 12 }}>
<div>📌 category: <b>{data.category}</b></div>
<div>🔥 importance: <b>{data.importance}</b></div>
<div style={{ marginTop: 8 }}>✅ todos:</div>
<ul>
{data.todos.map((t, i) => (
<li key={i}>
{t.title} {t.due ? <span>(🗓️ {t.due}</span> : null}
</li>
))}
</ul>
</div>
)}
</div>
);
}

「スキーマで縛ってるのに、さらにZodで検証するの?」って思うけど、ここはアプリ側の安全ベルトです🧷 スキーマは強いけど、現実の運用では“想定外”がゼロにならないので、最後はコードで守るのが安心です🙂 (Google AI for Developers)


Recovery Levels

④ JSONが壊れた時のリカバリ案(3段構え)🧯🧯🧯

レベル1:よくある“余計な文字”を削って再パース🧼

  • 先頭/末尾の空白
  • たまに混ざる説明文
export function tryParseLooseJson(text: string) {
// まずは素直に
try { return { ok: true as const, value: JSON.parse(text) }; } catch {}

// 次に「最初の{」〜「最後の}」だけ抜く(雑だけど効くことある)
const start = text.indexOf("{");
const end = text.lastIndexOf("}");
if (start >= 0 && end > start) {
const sliced = text.slice(start, end + 1);
try { return { ok: true as const, value: JSON.parse(sliced) }; } catch {}
}
return { ok: false as const };
}

レベル2:“修復専用AI”に直させる🔧🤖

ここが便利ポイント! Firebase AI LogicはsystemInstructionも設定できるので、「壊れたJSONを直して、JSONだけ返せ」を強制しやすいです🙂 (Firebase)

import { getGenerativeModel } from "firebase/ai";
import { ai, dailyReportSchema } from "./aiBase"; // aiはgetAI済みのもの

const repairModel = getGenerativeModel(ai, {
model: "GEMINI_MODEL_NAME",
systemInstruction:
"You are a JSON repair tool. Output ONLY valid JSON that matches the given schema. No prose.",
generationConfig: {
responseMimeType: "application/json",
responseSchema: dailyReportSchema,
},
});

export async function repairJson(broken: string) {
const prompt = `
Broken output:
${broken}

Fix it and return only JSON that matches the schema.
`.trim();

const result = await repairModel.generateContent(prompt);
return JSON.parse(result.response.text());
}

レベル3:UXで救う(再試行・手入力・プレーン表示)🧑‍🤝‍🧑

  • 「もう一回やる🔁」ボタンを出す
  • JSON抽出がダメなら、プレーン文章で表示して人が直せるルートを用意する
  • 失敗ログは“個人情報を入れずに”残す🧯

⑤ ちょい運用メモ(地味に超重要)📦🧠

  • スキーマは小さく:スキーマ自体も入力トークンに影響します(大きすぎると失敗しやすい)📉 (Firebase)
  • enumは強い:分類は enumString がとても安定します🎯 (Firebase)
  • モデルは入れ替わる:最近のドキュメントでもモデルの提供/引退が明記されてます。モデル名はRemote Configで差し替えられる設計にしておくと安全です🧯 (Firebase)

ミニ課題 🧪✨

  1. todosconfidence(0〜1の自信度)を追加してみる💡(制約はZodでチェック)
  2. category"progress" | "issue" | "plan" | "other" に増やして、otherの時だけ note を入れてもらう(optionalProperties を使う)🧩 (Firebase)
  3. 抽出結果を「カード → 編集 → 保存」できるUIにして、次章以降(サーバー側/評価)へ繋げる🚀

チェック ✅✅✅

  • JSONのキーが毎回ブレない(UIが壊れない)?🧱
  • optionalProperties を使って「無理に埋めさせない設計」になってる?🙂 (Firebase)
  • 壊れた時に「ユーザーが詰まない」逃げ道がある?🧯
  • 重要度や配列上限など、アプリ側で最終ガードできてる?🛡️

次の章に進むなら、ここで作ったJSONをそのまま使って「画像生成のメタ情報」や「NG表現チェック結果」みたいな“機械が扱うデータ”に広げると、気持ちよく繋がりますよ〜😆✨