🐢
zodで複雑なバリデーションを行うときはtransformを使うとスマートに書ける
これは何?
zodを使って複雑なバリデーションを行うときにtransform
を使うとスマートに書けるよという備忘録。今回はzodを使ってfile upload validationを行う。
環境
- Next.js 15.3.1
- Zod 3.23.8
- TypeScript 5.5.2
ユースケース
- プロフィール編集画面にアバター画像のアップロード機能がある
- 画像なしで運用したいユーザーもいるため、画像アップロードは必須ではない
- プロフィール画像はjpg,png,webp形式のみ対応
- ファイルサイズは5MBまで
初期実装
まず、ググって以下の実装を検討した。
import { z } from "zod";
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ACCEPTED_IMAGE_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
]; // jpg,png,webpのみ許可
export const fileSchema = z.object({
avatar: z
.instanceof(File) // ファイル型であることをチェック
.optional() // undefinedを許容
.refine((file) => {
if (!file) return true; // undefinedなら許可
return file.size <= MAX_FILE_SIZE;
}, "ファイルサイズは最大5MBまでです")
.refine((file) => {
if (!file) return true; // undefinedなら許可
return ACCEPTED_IMAGE_TYPES.includes(file.type);
}, "JPG、PNG、またはWEBP形式の画像のみアップロード可能です"),
});
しかしこの実装は意図どおり動かなかった。結局理由は分からなかったが、理解までに1hほど溶かしてしまった。なぜ時間をかけてしまったか考えたところ、以下の原因がありそうだと分かった。
初期実装の問題点
- チェインが4つ連続しており、合成される型が最終的にどうなるか推測しづらい。
-
refine()
の中のif (!file) return true;
は型エラーを起こさないが、正確に動くか不明
以上の理由を踏まえて、よりスマートな実装を検討した。
改良後の実装
いろいろ調べて、最終的にzodの公式に紹介されていたtransform
を使った実装を採用した。
import { z } from "zod";
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ACCEPTED_IMAGE_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
]; // jpg,png,webpのみ許可
export const fileSchema = z.object({
avatar: z.custom<File>().transform((file, ctx) => {
// ファイル存在チェック。
// 画像が未選択でも更新を許可するため、undefinedをreturn
if (file.size === 0) return undefined;
// ファイルサイズチェック
if (file.size > MAX_FILE_SIZE) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "ファイルサイズは最大5MBまでです",
});
}
// ファイル形式チェック
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "JPG、PNG、またはWEBP形式の画像のみアップロード可能です",
});
}
// チェックを突破したら、後続処理のためにfileをreturn
return file;
}),
});
参考)
改良後の実装の特徴
-
instanceof(File)
だと画像未選択時にエラーとなるため、custom<File>()
としてカスタムスキーマを作成。参考) https://zod.dev/?id=custom-schemas -
transform
内に3つのバリデーションロジックを記述。存在チェック、サイズチェック、形式チェックの3つを1つのロジック内にまとめて記述することでシンプルにした。
初期実装ではロジックの最終結果が予測しづらかったが、改良後の実装ではロジックの結果が予測しやすくなっていて、読みやすい。
まとめ
transform
を使って変換と検証を同時に行うと、メソッドチェインを沢山行うよりも読みやすいコードが書ける。superRefine
とかも使ってみたい。
Discussion