たとえば、顧客タイプが 'individual'(個人)の場合は companyName(会社名)は任意、'business'(法人)の場合は必須とする例になります。
1. discriminated union を使う方法
Zod の discriminated union を使うと、customerType の値ごとにスキーマを分けることができます。以下の例では、'individual' の場合は companyName が任意、'business' の場合は companyName が必須となります。
import { z } from 'zod';
const individualSchema = z.object({
customerType: z.literal('individual'),
companyName: z.string().optional(),
});
const businessSchema = z.object({
customerType: z.literal('business'),
companyName: z.string().nonempty('会社名は必須です。'),
});
const schema = z.discriminatedUnion('customerType', [
individualSchema,
businessSchema,
]);
// テスト例
console.log(schema.safeParse({ customerType: 'individual' })); // OK
console.log(schema.safeParse({ customerType: 'business', companyName: '' })); // エラー
この方法は、各ケースごとに独立したスキーマを定義できるため、読みやすく保守しやすいメリットがあります。
2. 各フィールドに refine を使う方法
companyName フィールドに対して .refine() を適用し、ctx.parent(親オブジェクト)を利用して条件付きバリデーションを実装する方法もあります。以下の例では、customerType が 'business' の場合のみ companyName の入力内容をチェックします。
import { z } from 'zod';
const schema = z.object({
customerType: z.enum(['individual', 'business']),
companyName: z.string().optional().refine((value, ctx) => {
// customerType が 'business' の場合のみ companyName の中身をチェック
if (ctx.parent.customerType === 'business') {
return value !== undefined && value.trim() !== '';
}
return true; // それ以外の場合は常に OK
}, {
message: '会社名は必須です。',
path: ['companyName'],
}),
});
// テスト例
console.log(schema.safeParse({ customerType: 'individual' })); // OK
console.log(schema.safeParse({ customerType: 'business', companyName: '' })); // エラー
この方法は、ひとつのスキーマ定義内で完結できるため、コード量を減らしたい場合に有効ですが、ctx.parent を利用している点や条件分岐が複雑になるとロジックの把握が難しくなる可能性があります。
どちらの方法も一長一短がありますので、プロジェクトの規模やコードの見通しやすさに応じて、適切な方法を選んでください。