You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

能否在Zod discriminatedUnion中预处理鉴别器实现字符串转数字?

Zod字符串转数字后用于discriminatedUnion的可行性及替代方案

可行性结论

不行,直接通过子schema的preprocess转换鉴别器值后用于z.discriminatedUnion不可行。原因是Zod的discriminatedUnion会先提取原始的鉴别器字段值,与各个分支的鉴别器字面量做匹配,这个过程发生在子schema的preprocess/transform之前。你传入"0"时,它会拿字符串"0"和分支里的数字0、字符串"2"/"3"对比,自然不匹配,导致验证失败。

替代方案

方案1:全局预处理鉴别器字段后使用discriminatedUnion

先对整个输入对象的brush_qty做预处理,将字符串转为数字,再传入discriminatedUnion验证。这样鉴别器匹配时用的就是转换后的值:

import { z } from "zod";

const BrushColorEnum = z.enum(
  ["BLUE_SILVER", "GREEN_SILVER", "RED", "GREEN_BLACK"],
  { message: "Invalid option" }
);

const BrushTypeEnum = z.enum(["THREAD", "MIXED", "CARLITE"], {
  message: "Invalid option",
});

const brushSchema = z.preprocess(
  (val) => {
    // 仅当输入是包含brush_qty的对象时,转换该字段
    if (typeof val === "object" && val !== null && "brush_qty" in val) {
      const qty = val.brush_qty;
      return {
        ...val,
        brush_qty: typeof qty === "string" ? parseInt(qty, 10) : qty
      };
    }
    return val;
  },
  z.discriminatedUnion("brush_qty", [
    z.object({
      brush_qty: z.literal(0),
      brush_type: z.union([z.string().length(0), z.undefined(), z.literal(false)]).transform(() => undefined),
      brush_color: z.union([z.string().length(0), z.undefined(), z.literal(false)]).transform(() => undefined),
    }),
    z.object({
      brush_qty: z.literal(2),
      brush_type: BrushTypeEnum,
      brush_color: BrushColorEnum,
    }),
    z.object({
      brush_qty: z.literal(3),
      brush_type: BrushTypeEnum,
      brush_color: BrushColorEnum,
    }),
  ])
);

// 测试:现在传入"0"也能成功
console.log(brushSchema.safeParse({ brush_qty: "0" })); // success
console.log(brushSchema.safeParse({ brush_qty: 0 })); // success
console.log(brushSchema.safeParse({ brush_qty: "2", brush_type: "THREAD", brush_color: "RED" })); // success

方案2:使用普通union配合refine自定义约束

如果不想全局预处理,可以先转换brush_qty为数字,再通过refine来关联各个字段的业务约束,实现类似discriminatedUnion的分支验证逻辑:

import { z } from "zod";

const BrushColorEnum = z.enum(
  ["BLUE_SILVER", "GREEN_SILVER", "RED", "GREEN_BLACK"],
  { message: "Invalid option" }
);

const BrushTypeEnum = z.enum(["THREAD", "MIXED", "CARLITE"], {
  message: "Invalid option",
});

const brushSchema = z.object({
  brush_qty: z.preprocess(
    (val) => typeof val === "string" ? parseInt(val, 10) : val,
    z.union([z.literal(0), z.literal(2), z.literal(3)])
  ),
  brush_type: z.union([z.string().length(0), z.undefined(), z.literal(false), BrushTypeEnum]),
  brush_color: z.union([z.string().length(0), z.undefined(), z.literal(false), BrushColorEnum]),
}).refine((data) => {
  if (data.brush_qty === 0) {
    // 无刷子时,brush_type和brush_color必须为空/undefined/false
    const isValidEmpty = (val) => val === undefined || val === false || val === "";
    return isValidEmpty(data.brush_type) && isValidEmpty(data.brush_color);
  } else {
    // 有刷子时,brush_type和brush_color必须是有效枚举值
    return BrushTypeEnum.safeParse(data.brush_type).success && BrushColorEnum.safeParse(data.brush_color).success;
  }
}, {
  message: "Brush type/color does not match quantity",
  // 可选:指定错误对应的字段路径
  path: ["brush_type", "brush_color"]
});

总结

  • z.discriminatedUnion的鉴别器匹配依赖原始输入值,子schema的预处理无法影响这一步,因此直接实现“转数字后鉴别”不可行。
  • 优先选择全局预处理+discriminatedUnion的方案,逻辑最清晰,和原生discriminatedUnion的分支验证体验一致;如果需要更灵活的约束,可使用union+refine的组合。

内容的提问来源于stack exchange,提问作者slinden

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 20:25:53