はじめに
Zod(v4)で条件付きバリデーションを実装しているとき、
条件に合致しているのにsuperRefineが呼ばれないケースに遭遇しました。
下記のような簡単なフォームにおいて、チェックインとチェックアウトの日付の相関チェックを行うケースで、
const schema = z
.object({
guests: z.number().int().min(1, "人数を選んでください"),
checkIn: z.iso.date("チェックイン日を入力してください"),
checkOut: z.iso.date("チェックアウト日を入力してください"),
})
.superRefine((data, ctx) => {
if (data.checkIn && data.checkOut && data.checkOut <= data.checkIn) {
ctx.addIssue({
code: "custom",
path: ["checkOut"],
message: "チェックアウトはチェックインより後にしてください",
});
}
});
チェックインとチェックアウトの日付が逆転していても、宿泊人数のバリデーションしか効いていません。
この記事では、superRefine でチェックしたかったのに他のエラーしか表示されない原因をZodの挙動から整理します。
superRefineが実行されていない原因
原因はzodの以下の挙動にありました。
- Zodはすべてのバリデーションを常に実行するわけではない
- 途中で他のバリデーションにかかり「aborted」になると、後続の処理は実行されない
つまり、superRefineは前段のバリデーションにおいて条件を満たした場合(abortedになっていない場合)のみ実行されるという設計になっているようでした。
Zodのステータス管理
では、どのような場合に後続処理(superRefine)が実行されなくなるのでしょうか。
Zodでは、バリデーションエラーの情報である「issues」 の中身によってabortedかを判定し、継続可否が決まる実装になっています。
Zodの継続判定
zodでは下記の通り状態を制御しています。
-
後続処理が継続可能なケース
- issues が 0 個
- issues が 1 個以上あるが、かつ継続不能な issue が含まれていない
(→ すべてcontinue === true 扱い)
-
後続処理が継続不可なケース
- payload.aborted === true または continue !== true の issue が存在する
Zodの処理順
次に、Zodがどの順番で処理を進めるのか
(→ どこで issue が追加され、どこでabortedの判定がされるのか)
を確認しました。(誤っている箇所あれば指摘ください。。)
Zod v4.3.6時点で記事を書いています。
処理の流れ
Zodは、概念的には次の段階で進むようです。
1. parse
- そのスキーマ本体の検証
2. runChecks
- parseで追加されたissuesからabortedかを判定
- checks(refine/superRefine など)を順に実行
- issueが増えるたびにabortedかを判定
- 各チェックを行う際にabortedなら後続checksをスキップ
- parse
- runChecks
今回の例でsuperRefine が実行されなかった理由
今回のフォーム例では、
guests: z.number().int().min(1)
でguestsがnumber型ではないため、invalid_typeが発生します。
invalid_typeのissueにはcontinueが指定されていないため、aborted() の continue !== true 条件に該当して後続の処理が実行されなくなります。
解決策
下記のようにrefineにwhenパラメータをつけることで解決できます。
whenパラメータをつけることで、通常のabortedでスキップ判定されず、explicitlyAbortedで実行判定されます。
explicitlyAbortedではcontinue === falseのissueがある場合にAbortedとなるため今回のケースではwhenをつけることで後続処理も実行されます。
const schema = z
.object({
guests: z.number().int().min(1, "人数を選んでください"),
checkIn: z.iso.date("チェックイン日を入力してください"),
checkOut: z.iso.date("チェックアウト日を入力してください"),
})
// superRefineのIFではwhenを指定できないのでrefineを使用
.refine((d) => !d.checkIn || !d.checkOut || d.checkOut > d.checkIn, {
message: "チェックアウトはチェックインより後にしてください",
path: ["checkOut"],
when: () => true, // whenを追加
});
まとめ
ZodでsuperRefineが効かない問題の原因を確認しました。
refine / superRefineを使用する際には理解しておかないとバリデーションがかかるタイミングがずれてしまうので注意が必要でした。
参考にさせていただいた記事

