1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ZodのsuperRefineの実行タイミングを理解したい

1
Posted at

はじめに

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: "チェックアウトはチェックインより後にしてください",
      });
    }
  });

image.png

チェックインとチェックアウトの日付が逆転していても、宿泊人数のバリデーションしか効いていません。

この記事では、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を追加
  });

image.png

まとめ

ZodでsuperRefineが効かない問題の原因を確認しました。
refine / superRefineを使用する際には理解しておかないとバリデーションがかかるタイミングがずれてしまうので注意が必要でした。

参考にさせていただいた記事

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?