如何在Zod中结合superRefine实现带动态验证的判别联合
解决方案:分离公共字段与公共校验,在扩展后统一应用
核心问题是superRefine会将Zod Object转换为Zod Effect类型,而Effect不支持merge等Object Schema的扩展方法。解决思路是让Base Schema保持纯Object类型,将公共校验逻辑抽离,在扩展完成后再应用校验。
方法1:抽离公共校验函数,扩展后追加校验
先定义仅包含公共字段的Base Schema,再把公共复杂校验写成独立函数,最后在扩展后的Schema上调用superRefine应用公共校验,再添加动态校验逻辑。
import { z } from "zod"; // 1. 定义纯Object类型的Base Schema(无superRefine/refine) const baseSchema = z.object({ id: z.string().uuid(), createdAt: z.date(), status: z.enum(["active", "inactive"]), }); // 2. 抽离公共复杂校验函数 const commonValidation = (data: z.infer<typeof baseSchema>, ctx: z.RefinementCtx) => { // 示例:公共跨字段校验 - 已失效的条目不能有未来的创建时间 if (data.status === "inactive" && data.createdAt > new Date()) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "已失效条目的创建时间不能晚于当前时间", path: ["createdAt"], }); } }; // 3. 动态扩展Schema的工厂函数 const createDynamicSchema = (externalFlag: boolean) => { // 先扩展Base Schema,添加业务字段 const extendedSchema = baseSchema.merge(z.object({ code: z.enum(["A", "B", "C"]), payload: z.record(z.string(), z.any()), })); // 先应用公共校验,再添加基于code和外部flag的动态校验 return extendedSchema .superRefine(commonValidation) .superRefine((data, ctx) => { // 动态校验逻辑示例 switch (data.code) { case "A": if (!externalFlag && !data.payload["requiredKey"]) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "code为A且外部flag关闭时,payload必须包含requiredKey", path: ["payload", "requiredKey"], }); } break; case "B": if (externalFlag && data.payload["maxValue"] > 100) { ctx.addIssue({ code: z.ZodIssueCode.too_big, maximum: 100, inclusive: true, path: ["payload", "maxValue"], message: "code为B且外部flag开启时,maxValue不能超过100", }); } break; } }); }; // 使用示例 const schemaForFlagOn = createDynamicSchema(true); const result = schemaForFlagOn.safeParse({ id: "3fa85f64-5717-4562-b3fc-2c963f66afa6", createdAt: new Date(), status: "active", code: "B", payload: { maxValue: 90 }, });
方法2:将公共校验封装为独立Schema,使用pipe串联
如果公共校验逻辑复杂,可以将其封装为独立的Zod Effect Schema,再通过pipe方法与扩展后的Object Schema串联,同样能实现保留公共校验+动态扩展的效果。
import { z } from "zod"; const baseSchema = z.object({ id: z.string().uuid(), createdAt: z.date(), }); // 封装公共校验为独立的Effect Schema const commonValidationSchema = z.superRefine((data: z.infer<typeof baseSchema>, ctx) => { if (data.createdAt > new Date()) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "创建时间不能晚于当前时间", }); } }); const createDynamicSchema = (externalFlag: boolean) => { const extendedSchema = baseSchema.merge(z.object({ code: z.enum(["X", "Y"]), count: z.number(), })); // 先串联公共校验,再添加动态校验 return extendedSchema .pipe(commonValidationSchema) .superRefine((data, ctx) => { if (data.code === "X" && externalFlag && data.count < 5) { ctx.addIssue({ code: z.ZodIssueCode.too_small, minimum: 5, inclusive: true, path: ["count"], message: "code为X且外部flag开启时,count不能小于5", }); } }); };
关键思路总结
- 让Base Schema保持纯ZodObject类型,只包含字段定义和简单校验(如
uuid()、enum()),避免过早使用superRefine/refine转换为Effect类型。 - 公共复杂校验逻辑抽离为独立函数或Schema,在扩展完成后再应用到Schema上。
- 通过工厂函数接收外部动态参数(如
externalFlag),在内部完成扩展、公共校验、动态校验的串联。
内容的提问来源于stack exchange,提问作者Cristian Flórez
相关产品推荐
相关产品推荐

