Closed5
大きくなりがちなZodスキーマの対処法メモ

以下は、React Hook Form + Zod を使ったフォームバリデーションで、条件によって表示される項目が変わりスキーマ定義が肥大化しがちな場合の対処法をまとめたメモ。

スキーマを分割してモジュール化する
大きくなったスキーマは、機能ごとにスキーマを分割することで見通しをよくする。
メインのスキーマファイルで、分割したスキーマを合成する。
例:下記のようにparticipationSchema.ts, addressSchema.ts のようにドメインごとにファイルをわける
// participationSchema.ts
import { z } from "zod";
export const participationSchema = z.object({
participation: z.enum(["participate", "notParticipate"]),
place: z.string().optional(), // 参加時のみ必須にしたい場合は別途 refine などで対応
});
// addressSchema.ts
import { z } from "zod";
export const addressSchema = z.object({
postalCode: z.string(),
prefecture: z.string(),
city: z.string(),
});
// mainSchema.ts
import { z } from "zod";
import { participationSchema } from "./participationSchema";
import { addressSchema } from "./addressSchema";
export const mainSchema = z
.object({
// 他にも必要なスキーマがあればここで
})
.merge(participationSchema)
.merge(addressSchema);
// z.object({ ...participationSchema.shape, ...addressSchema.shape }) みたいな感じでmergeも可能らしい

z.discriminatedUnionやz.union を活用する
import { z } from "zod";
const participationUnionSchema = z.discriminatedUnion("participation", [
z.object({
participation: z.literal("participate"),
place: z.string().min(1, "参加場所は必須です"),
// 参加する場合のみ必要な項目
}),
z.object({
participation: z.literal("notParticipate"),
// 不参加の場合は項目なし or 別途何かがあればここに追加
}),
]);
export type ParticipationFormType = z.infer<typeof participationUnionSchema>;
React Hook Form と組み合わせる際は、useForm の resolver にこのスキーマをそのまま渡して使用

不要なフィールドはoptionalにして、refineで条件にしたバリデーションを実装
- 状況によって表示されないフィールドをoptionalとして定義し、Zod の refine や superRefine で「条件が合致する場合は必須にする」などのロジックを追加
- 条件分岐の数が多いとスキーマが煩雑になるので、細分化して対処できるなら discriminatedUnion や複数スキーマの合成を優先する
// baseSchema.ts
import { z } from "zod";
export const baseSchema = z
.object({
participation: z.enum(["participate", "notParticipate"]),
place: z.string().optional(),
})
.refine(
(data) => {
if (data.participation === "participate") {
return !!data.place; // place が必須
}
return true;
},
{
message: "参加場所は必須です",
path: ["place"],
}
);

あとは型エイリアスで対応とか?
このスクラップは2025/01/19にクローズされました