Zodでのパスワード一致検証の実装方法
要点まとめ
- Zodでは
.refine()
か.superRefine()
でカスタムバリデーションを実装 - パスワードと確認用パスワードの比較に最適な手法
path
オプションでエラーを特定フィールドに表示可能- React Hook Formなどのフォームライブラリと容易に統合可能
問題の背景
ユーザー登録フォームなどでパスワード入力時に必要な「パスワード確認」機能をZodで実装したいケースです。元のスキーマではpassword
とconfirmPassword
が独立して検証されているため、二つの値が一致しているかのチェックが不足しています。
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"] // エラーを表示するフィールド指定
}
);
動作の詳細
- 検証のタイミング: すべての基本バリデーション(文字列形式、最小長さなど)が成功した後に実行
- エラー表示:
path
で指定したフィールドにエラーメッセージが紐付けられる - データアクセス: コールバック関数の引数でオブジェクト全体にアクセス可能
注意点
基本バリデーションに失敗すると、.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の比較
特徴 | refine | superRefine |
---|---|---|
複数エラーの追加 | ❌ | ✅ |
複雑なバリデーションロジック | △ | ◎ |
シンプルな値比較 | ◎ | △ |
型推論の安定性 | ◎ | △ |
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のバリデーションは順次実行され、基本ルールが満たされないと後続のカスタムチェックは実行されません。この設計は以下の理由によります:
- 重複エラーを防ぐ
- 基本的なエラーを優先表示
- パフォーマンス最適化
パスワードのみではなく両方のフィールドにエラーを表示するには?
superRefine
を使用し、複数のエラーを追加します:
ts
.superRefine(({ password, confirmPassword }, ctx) => {
if (password !== confirmPassword) {
ctx.addIssue({ path: ['password'], /*...*/ });
ctx.addIssue({ path: ['confirmPassword'], /*...*/ });
}
})