Firestoreのデータ操作時にZodでバリデーションする
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