Skip to content

Zodでのパスワード一致検証の実装方法

要点まとめ

  • Zodでは.refine().superRefine()でカスタムバリデーションを実装
  • パスワードと確認用パスワードの比較に最適な手法
  • pathオプションでエラーを特定フィールドに表示可能
  • React Hook Formなどのフォームライブラリと容易に統合可能

問題の背景

ユーザー登録フォームなどでパスワード入力時に必要な「パスワード確認」機能をZodで実装したいケースです。元のスキーマではpasswordconfirmPasswordが独立して検証されているため、二つの値が一致しているかのチェックが不足しています。

ts
// 問題点: passwordとconfirmPasswordの一致チェックがない
export const registerUserSchema = z.object({
  password: z.string().min(4),
  confirmPassword: z.string().min(4),
  // ...他のフィールド
});

解決策: refineメソッドによるカスタムバリデーション

Zodでは.refine()メソッドを使用して、複数フィールドにまたがるカスタムバリデーションロジックを追加できます。

基本的な実装方法

ts
export const registerUserSchema = z.object({
  // ...他のフィールド
  password: z.string().min(4),
  confirmPassword: z.string().min(4),
}).refine(
  (data) => data.password === data.confirmPassword,
  {
    message: "パスワードが一致しません",
    path: ["confirmPassword"] // エラーを表示するフィールド指定
  }
);

動作の詳細

  1. 検証のタイミング: すべての基本バリデーション(文字列形式、最小長さなど)が成功した後に実行
  2. エラー表示: pathで指定したフィールドにエラーメッセージが紐付けられる
  3. データアクセス: コールバック関数の引数でオブジェクト全体にアクセス可能

注意点

基本バリデーションに失敗すると、.refine()は実行されません 例: passwordが4文字未満の場合 → confirmPasswordの一致チェックはスキップ

superRefineを使った代替方法

より高度な制御が必要な場合は.superRefine()を使用します。複数のエラーを追加できる点が特徴です。

ts
export const registerUserSchema = z.object({
  // ...フィールド定義
}).superRefine(({ password, confirmPassword }, ctx) => {
  if (confirmPassword !== password) {
    ctx.addIssue({
      code: "custom",
      message: "パスワードが一致しません",
      path: ['confirmPassword']
    });
  }
});

refineとsuperRefineの比較

特徴refinesuperRefine
複数エラーの追加
複雑なバリデーションロジック
シンプルな値比較
型推論の安定性

React Hook Formとの統合

ZodのスキーマはReact Hook Formとシームレスに統合可能です。

tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const FormComponent = () => {
  const { register, handleSubmit, formState } = useForm({
    resolver: zodResolver(registerUserSchema)
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('password')} type="password" />
      <input {...register('confirmPassword')} type="password" />
      {formState.errors.confirmPassword && (
        <p>{formState.errors.confirmPassword.message}</p>
      )}
      {/* ...他のフィールド */}
    </form>
  );
};

ベストプラクティスと応用例

1. エラーメッセージの国際化対応

ts
.refine(/*...*/, {
  message: "パスワードが一致しません", // 日本語メッセージ
  // 国際化: 言語に応じたメッセージ切替が可能
})

2. その他のユースケースへの拡張

パスワード以外の一致検証にも応用可能です:

ts
// メールアドレス確認
.refine(data => data.email === data.confirmEmail, { /*...*/ })

// 新しいパスワードの一致確認
.refine(data => data.newPassword === data.confirmNewPassword, { /*...*/ })

// 条件付き必須フィールド(保険証番号の入力が必要な場合のみ)
.refine(data => 
  data.requiresInsurance ? !!data.insuranceNumber : true, 
  { path: ['insuranceNumber'] }
)

3. テストの実施

ts
test('パスワードが一致しない場合にエラーを返す', () => {
  const invalidData = {
    password: 'password123',
    confirmPassword: 'differentPassword'
  };

  const result = registerUserSchema.safeParse(invalidData);
  expect(result.success).toBe(false);
  expect(result.error.issues[0].message).toBe('パスワードが一致しません');
});

よくある質問

基本バリデーションに失敗した場合、確認パスワードエラーは表示されないのはなぜですか?

Zodのバリデーションは順次実行され、基本ルールが満たされないと後続のカスタムチェックは実行されません。この設計は以下の理由によります:

  1. 重複エラーを防ぐ
  2. 基本的なエラーを優先表示
  3. パフォーマンス最適化
パスワードのみではなく両方のフィールドにエラーを表示するには?

superRefineを使用し、複数のエラーを追加します:

ts
.superRefine(({ password, confirmPassword }, ctx) => {
  if (password !== confirmPassword) {
    ctx.addIssue({ path: ['password'], /*...*/ });
    ctx.addIssue({ path: ['confirmPassword'], /*...*/ });
  }
})