🏌️

Firestoreのデータ操作時にZodでバリデーションする

2024/12/12に公開

Firestoreは半構造化データベースであり、RDBほど厳密なスキーマ設計を必要としません。
そのため、Firestoreではデータの保存において柔軟性があり、同コレクション内のそれぞれのドキュメントに異なるフィールドを持たせることが可能です。
しかし、この柔軟性がある一方で、データの整合性を保つ必要性が高い場合にはバリデーションが重要になります。

TypeScriptは静的型チェックを提供しますが、実行時のデータバリデーションを行う機能はありません。そのためZod等を使うことで、Firestoreなど外部から取得したデータが意図した型であるかを実行時にチェックすることができます。

今回は、Next.jsのアプリケーションにおいてFirestoreからデータを取得する際に、Zodでバリデーションを実施してみます。

Zodスキーマの定義

Zodを使って、Firestoreで取得するuserデータの型を定義します。
(src/lib/zodSchemas.ts)

import { z } from "zod";

// ユーザーのスキーマ定義
export const UserSchema = z.object({
  name: z.string(),
  age: z.number().int(),
  createdAt: z.date(),
});

export type User = z.infer<typeof UserSchema>;
  • User 型はzodのスキーマを基に型推論されます。これにより、Firestoreから取得したデータが正しい型かどうかをチェックできます。

Firestoreの初期化

次に、Firestoreを初期化するためのコードです。
(src/server/lib/firebaseAdmin.ts)

import "server-only";
import { applicationDefault, getApps, initializeApp } from "firebase-admin/app";
import { getFirestore } from "firebase-admin/firestore";

// Firebase Admin SDKの初期化
if (!getApps().length) {
  initializeApp({
    credential: applicationDefault(), // 認証情報を使用
  });
}

export const db = getFirestore(); // Firestoreインスタンスのエクスポート
  • initializeAppを使ってFirebase Admin SDKを初期化し、dbとしてFirestoreインスタンスをエクスポートします。

ユーザーデータの取得

Firestoreからデータを取得し、Zodでバリデーションを行います。
(src/server/lib/db/user.ts)

import "server-only";
import { db } from "../firebaseAdmin";
import { UserSchema } from "@/lib/zodSchemas";

export async function getUsers() {
  const snapshot = await db.collection("user").get();

  const users = snapshot.docs.map((doc) => {
    const userData = doc.data();

    // Zodでバリデーションを行う
    const validatedUser = UserSchema.parse({
      ...userData,
      createdAt: userData.createdAt.toDate(),
    });

    return { docId: doc.id, ...validatedUser };
  });

  return users;
}
  • getUsers 関数で、Firestoreのuserコレクションからデータを取得し、UserSchema.parse()でバリデーションを行います。createdAtはFirestoreのタイムスタンプ型で取得されるため、toDate()メソッドを使ってDate型に変換しています。

  • Zodのparse()メソッドは、データがスキーマに一致しない場合、実行時にエラーを投げます。このため、データが予期した型であることを確実に検証できます。エラーが出た際の処理はアプリケーションによって適切な対応が必要になります。

ユーザーデータの表示

Firestoreから取得したユーザー情報を表示するUserListコンポーネントを作成します。
このコンポーネントでは、getUsers()で取得したユーザー情報をリストとして表示します。

(src/app/_components/UserList.tsx)

import { getUsers } from "@/server/lib/db/user";

export async function UserList() {
  const users = await getUsers();

  return (
    <div>
      <h2>ユーザー</h2>
      <ul className="flex flex-col gap-2">
        {users.map((user) => (
          <li key={user.docId} className="border-b py-2">
            <p><strong>名前:</strong> {user.name}</p>
            <p><strong>年齢:</strong> {user.age}</p>
            <p><strong>作成日時:</strong> {user.createdAt.toLocaleString()}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

次に、このUserListコンポーネントをpage.tsxに組み込みます。

(src/app/page.tsx)

import { UserList } from "./_components/UserList";

export default function Home() {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen p-8">
      <main className="text-center">
        <h1 className="text-2xl font-bold mb-4">Welcome to Next.js!</h1>
        <UserList />  {/* UserListコンポーネントを表示 */}
      </main>
    </div>
  );
}

firestoreから取得したドキュメントの各フィールドが意図した型であれば、UIにデータが表示されます。

もし、nameフィールドがなかったり、ageフィールドがnumberでなかった場合はエラーになります。

まとめ

  • Zodを使ったバリデーションにより、Firestoreから取得したデータが型安全であることを確実にすることができます。これにより、型エラーや不正なデータを早期に検出できます。
  • データ挿入時にもZodを使ったバリデーションを実施し、不正なデータがFirestoreに保存されることを防ぐことが重要です。
  • 取得したデータが型に一致しない場合のエラー処理を適切に行い、アプリケーションが予期しないデータによって壊れないようにします。

Discussion