Zod discriminatedUnion 中如何为不同类型添加 refine?
结论
如果项目使用 Zod 3,传给 z.discriminatedUnion 的分支应保持为普通 z.object,不要先调用 refine。调用 refine 会将 schema 包装成 ZodEffects,而 Zod 3 的 discriminatedUnion 要求分支是对象 schema。可以在联合类型所在的外层对象上调用 superRefine,再根据 type 校验各个分支。
示例中的 refine 回调也应返回布尔值。返回错误字符串不会将它用作错误消息,因为非空字符串是真值,可能导致校验通过。
将分支校验放到联合类型之后
先将各分支保留为普通对象,再根据解析后的数据和判别字段执行校验:
const specificDataA = z.object({
type: z.literal("typeA"),
isSomeDataRequired: z.boolean(),
someData: z.string().optional()
});
const specificDataB = z.object({
type: z.literal("typeB"),
// typeB 的字段
});
const mySchema = baseTypeSchema
.extend({
id: z.string(),
name: z.string(),
specificData: z.discriminatedUnion("type", [specificDataA, specificDataB] as const)
})
.superRefine((data, ctx) => {
const specificData = data.specificData;
if (
specificData.type === "typeA" &&
specificData.isSomeDataRequired &&
specificData.someData === undefined
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["specificData", "someData"],
message: "someData is required when isSomeDataRequired is true"
});
}
// 可以在这里按其他 type 添加对应的分支校验。
});
判别字段 type 会让 TypeScript 将 specificData 收窄到对应分支。检查 type === "typeA" 后,就可以安全读取 isSomeDataRequired 和 someData。path 指向外层对象中的 specificData.someData,错误会定位到这个字段。
如果单独校验某个分支
如果调用方还需要单独解析 specificDataA,可以保留基础对象 schema,并在独立的验证函数中返回布尔值,或抛出带有明确错误信息的校验结果。不要将经过 refine 包装的 schema 放入 Zod 3 的 discriminatedUnion 分支数组。
还需确认“必填”的具体含义。如果 someData 只需存在,检查 someData === undefined 即可,空字符串也有效。如果还要求非空,可以在字段上使用 z.string().min(1),并根据业务条件设计相应校验。
以上写法针对 Zod 3 中的 ZodEffects 行为。如果项目使用其他主版本,请以该版本的 schema 类型和 API 为准。
备注:内容仅供参考。
