Closed4

ZodのdiscriminatedUnion と union

さんやまさんやま

union

いくつかのスキーマを並べて、「いずれかにマッチすれば OK」というチェックを行うための関数
以下の例だと文字列でも数値でもバリデーションが通るようになる

const schema = z.union([
  z.string(),
  z.number(),
]);

さんやまさんやま

discriminatedUnion

z.discriminatedUnion("キー", [ スキーマA, スキーマB, ... ]) のように書くと、指定した「キー」の値をディスクリミネータとして使い、キーの値に応じて排他的にスキーマを切り替えることができる

  • メリット:
    • 「どの分岐を使うべきか」をキーの値で自動的に判断するため、バリデーションエラーが分かりやすく、意図しないスキーマとの混同を防げる
    • 「参加」「不参加」など明確に分けられる選択肢がある場合に非常に便利
さんやまさんやま

ラジオボタンで条件分岐する場合

discriminatedUnion を使わない場合

例えば、会社行事に「参加」「不参加」のラジオボタンがあるフォームを考えます。参加の場合は「参加場所」の入力が必須で、不参加の場合は入力項目が不要とする。

  1. 素朴な書き方
import { z } from "zod";

const formSchema = z.object({
  participation: z.enum(["participate", "notParticipate"]), // ラジオボタン
  place: z.string().optional(), // 参加したら必須
}).refine((data) => {
  // participate のときは place が必要
  if (data.participation === "participate") {
    return Boolean(data.place);
  }
  return true;
}, {
  message: "参加の場合は場所が必須です",
  path: ["place"],
});

スキーマとしてはキーマとしては単一の z.object となり、participation が "participate" か "notParticipate" かを見て追加のチェックを行っている。
条件分岐のパターンが増えると、refine の中身が長くなり、可読性が下がる。

さんやまさんやま

discriminatedUnion を使った場合

import { z } from "zod";

const participateSchema = z.object({
  participation: z.literal("participate"),
  place: z.string().min(1, "参加場所は必須です"),
  // 必要に応じて他のフィールドを追加
});

const notParticipateSchema = z.object({
  participation: z.literal("notParticipate"),
  // 不参加の場合は入力項目なし あるいは別のフィールドを追加可能
});

export const eventFormSchema = z.discriminatedUnion("participation", [
  participateSchema,
  notParticipateSchema,
]);

// 型定義は z.infer ですぐ取得
export type EventFormType = z.infer<typeof eventFormSchema>;

  1. ディスクリミネータ(participation)で排他的に選択
  • participation: z.literal("participate")
    こちらがマッチした場合、participateSchema が適用される
  • participation: z.literal("notParticipate")
    こちらがマッチした場合、notParticipateSchema が適用される

こうすることで、「参加」か「不参加」かをディスクリミネータで自動判別して、必要な項目が自動的にチェックされる。
もしフォーム入力で participation: "participate" が選択されていれば participateSchema が動作し、place が必須となる。
逆に participation: "notParticipate" であれば participateSchema にマッチしないので、place のバリデーションは不要とみなされる。

このスクラップは2025/01/19にクローズされました