ymmr
Recent Articles
    公開日: 2024/04/06

    Zod の refine を理解する

    Zod で独自のスキーマバリデーションを実現するには、refine というメソッドを使います。また、さらに高度な使い方ができる superRefine が提供されているため、使い方を復習してみようと思います。

    refine とは

    refinerefinement のエイリアスとして提供されており、素材などを変換する (Zod でいうスキーマの型を再定義) という意味で使われます。Zod は数多くのスキーマを提供していますが、独自で定義したい場面も少なくありません。

    import z from 'zod';
    
    // Zod は整数だけでなく、自然数のようなスキーマも提供している。
    const numberSchema = z.number().positive();
    
    console.log(numberSchema.safeParse(12)); // { success: true, data: 12 }
    
    // 偶数のスキーマは独自で定義する。
    const evenNumberSchema = z.number().refine((num) => num % 2 === 0);
    
    evenNumberSchema.parse(5); // 例外発生
    

    より高度な使い方

    refine の第 2 引数を使うことで、エラーハンドリングの振る舞いを細かく調整できます。React Hook Form と組み合わせたフォームバリデーションでは、エラーが発生したフィールドを特定するといった用途で用います。

    import z from 'zod';
    
    export function validatePassword(credential: {
      userId: string;
      password: string;
    }) {
      const credentialSchema = z
        .object({
          userId: z.string(),
          password: z.string(),
        })
        .refine(({ password }) => password.length > 8, {
          message: 'パスワードは 8 文字以上に設定してください。',
          path: ['password'],
          // ^^^^ フィールドを特定する値を path の配列内で指定する。
        });
    
      credentialSchema.parse(credential);
    }
    

    検証に失敗すると、Zod は次の様なエラーを吐きます。そのため、パスワードの入力フィールドにフォーカスを当てるといった実装ができるわけです。

    ZodError {
      issues: [{
        'code': 'custom',
        'path': [ 'password' ],
        'message': 'パスワードは 8 文字以上に設定してください。'
      }]
    }
    

    superRefine

    実のところ、refinesuperRefine のシンタックスシュガーです。よって、より複雑な検証には superRefine が必要になります。
    ここではインターフェースの確認にとどめますが、ZodIssue をカスタマイズしたり、検証を途中で止めたりできる様です。上記のコードを superRefine で書き換えてみましょう。

           userId: z.string(),
           password: z.string(),
         })
    -    .refine(({ password }) => password.length > 8, {
    -      message: 'password must be longer than 8 characters.',
    -      path: ['password'],
    +    .superRefine(({ password }, context) => {
    +      if (password.length <= 8) {
    +        context.addIssue({
    +          code: 'custom',
    +          message: 'password must be longer than 8 characters',
    +          path: ['password'],
    +        });
    +      }
         });
    
       credentialSchema.parse(credential);
    

    見ての通り ZodIssue を定義して、ZodError を自力で組み立てていきます。そのため、refine より柔軟なカスタマイズができるというわけです。

    おわりに

    今回は Zod の refine 周りを復習しました。フォームバリデーションや API との繋ぎ込みなど、用途は広いのでしっかりと押さえておきたいところです。