0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TypeScript】AIの「嘘」を型で見抜く!Zod × OpenAIで実現する堅牢なStructured Output開発 🛡️🤖

Posted at

こんにちは😊
株式会社プロドウガ@YushiYamamotoです!
らくらくサイトの開発・運営を担当しながら、React.js・Next.js専門のフリーランスエンジニアとしても活動しています❗️

この記事は TypeScript Advent Calendar 2025 の参加記事です🎄

2025年現在、Web開発において「生成AI(LLM)の組み込み」は当たり前になりました。
しかし、TypeScript使いの私たちには一つの悩みがあります。

「AIが返してくるJSON、本当にその型であってる?」

JSON.parse() した結果を as MyType でキャストして安心していませんか?
AIは平気で嘘をつきますし、たまにJSONの構造を間違えます。ランタイムで型が合わずにアプリが落ちる……そんな悪夢を防ぐための、TypeScript × Zod × AI の鉄板パターンをご紹介します。

😨 as キャストの危険性

AIを使って「料理のレシピ」を生成する機能を考えてみましょう。
私たちは以下のような型を期待しています。

type Recipe = {
  title: string;
  ingredients: string[];
  cookingTime: number;
};

しかし、OpenAI APIから返ってくるのはただの string です。
これをこう書いてしまうのは、非常に危険です。

// 💀 危険なコード
const response = await openai.chat.completions.create({ ... });
const jsonString = response.choices[0].message.content;
// ここでAIが { "name": "カレー" } みたいな違うキーを返してきたら爆発する
const recipe = JSON.parse(jsonString) as Recipe; 

TypeScriptはコンパイル時にはエラーを出しませんが、実行時に recipe.title にアクセスした瞬間、undefined になりバグを生みます。

🛡️ Zodで「実行時バリデーション」を行う

そこで登場するのが、スキーマバリデーションライブラリの Zod です。
Zodを使えば、「TypeScriptの型定義」と「実行時のチェックロジック」を同時に定義できます。

ステップ1:スキーマの定義

まずはZodでスキーマを定義し、そこからTypeScriptの型を抽出します。
これで「型定義」と「バリデーションルール」の二重管理がなくなります。

import { z } from "zod";

// Zodスキーマの定義
const RecipeSchema = z.object({
  title: z.string().describe("料理のタイトル"),
  ingredients: z.array(z.string()).describe("材料のリスト"),
  cookingTime: z.number().int().min(1).describe("調理時間(分)"),
  difficulty: z.enum(["easy", "medium", "hard"]).describe("難易度"),
});

// スキーマからTypeScriptの型を自動生成
type Recipe = z.infer<typeof RecipeSchema>;

describe() が重要!
Zodの .describe() に書いた説明文は、後述するOpenAIのStructured Outputs機能を使う際に、AIへのヒントとして渡されます。「型」自体がプロンプトになるのです。

🤖 OpenAI Structured Outputs との連携

OpenAIの response_format にZodスキーマを渡すことで、AIに対して「このJSONスキーマ以外は絶対に返さないでくれ」と強制できます。
(※ zod-response-format ヘルパーなどを使用する想定)

実装コード例
import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";

const openai = new OpenAI();

// スキーマ定義(前述と同じ)
const RecipeSchema = z.object({
  title: z.string(),
  ingredients: z.array(z.string()),
  cookingTime: z.number(),
  difficulty: z.enum(["easy", "medium", "hard"]),
});

async function generateRecipe(keyword: string) {
  const completion = await openai.chat.completions.create({
    model: "gpt-4o-2024-08-06", // Structured Outputs対応モデル
    messages: [
      { role: "system", content: "あなたはプロの料理人です。" },
      { role: "user", content: `${keyword}を使ったレシピを考えて。` },
    ],
    // ここでZodスキーマを渡す!
    response_format: zodResponseFormat(RecipeSchema, "recipe"),
  });

  const content = completion.choices[0].message.content;

  // 念のためZodでパースする(二重の防御)
  // AIが万が一フォーマットを破っても、ここで検知して安全にエラーハンドリングできる
  const result = RecipeSchema.safeParse(JSON.parse(content || "{}"));

  if (!result.success) {
    console.error("AIが不正なJSONを返しました", result.error);
    throw new Error("レシピ生成に失敗しました");
  }

  // ここでは result.data は確実に Recipe 型であることが保証される
  return result.data; 
}

💡 なぜこの構成が最強なのか?

  1. 型安全性の保証: result.data はランタイムでも確実に型と一致しています。undefined アクセスによるクラッシュを防げます。
  2. プロンプト管理の効率化: Zodスキーマを書くだけで、それがそのままAIへの「出力指示書」になります。プロンプトで「JSONで返して。キーはtitleで...」と長々と書く必要がなくなります。
  3. 開発体験(DX)の向上: VS Codeなどのエディタで、AIのレスポンスに対して完璧な補完が効きます。

まとめ

TypeScriptを使う最大のメリットは「安心感」です。
しかし、外部API(特にAI)との境界線では、その安心感が崩れがちです。

「AIを信じるな、Zod(スキーマ)を信じろ」

これを合言葉に、2025年も堅牢なTypeScriptライフを送りましょう!🚀

最後に:業務委託のご相談を承ります

私は業務委託エンジニアとしてWEB制作やシステム開発を請け負っています。最新技術を活用したレスポンシブなWebサイト制作、インタラクティブなアプリケーション開発、API連携など幅広いご要望に対応可能です。

「課題解決に向けた即戦力が欲しい」「高品質なWeb制作を依頼したい」という方は、お気軽にご相談ください。一緒にビジネスの成長を目指しましょう!

👉 ポートフォリオ

🌳 らくらくサイト

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?