🐢

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ほど溶かしてしまった。なぜ時間をかけてしまったか考えたところ、以下の原因がありそうだと分かった。

初期実装の問題点

  1. チェインが4つ連続しており、合成される型が最終的にどうなるか推測しづらい。
  2. 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;
  }),
});

参考)
https://zod.dev/?id=validating-during-transform

改良後の実装の特徴

  • instanceof(File)だと画像未選択時にエラーとなるため、custom<File>()としてカスタムスキーマを作成。参考) https://zod.dev/?id=custom-schemas
  • transform内に3つのバリデーションロジックを記述。存在チェック、サイズチェック、形式チェックの3つを1つのロジック内にまとめて記述することでシンプルにした。

初期実装ではロジックの最終結果が予測しづらかったが、改良後の実装ではロジックの結果が予測しやすくなっていて、読みやすい。

まとめ

transformを使って変換と検証を同時に行うと、メソッドチェインを沢山行うよりも読みやすいコードが書ける。superRefineとかも使ってみたい。

Discussion