PAYATHON 2026

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 为准。

备注:内容仅供参考。