DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
OnlineSIM:在线接收短信,虚拟号码租用

Zod multipleOf(2) 接受大奇数,安全整数检查仍不够

4.6.7的浮点倍数容差会随商扩大,吞掉大安全整数的半整数差。对只允许安全偶数的字段明确检查余数,区分小数契约与输入先失真。

z.number().multipleOf(2) 本来应拒绝奇数,数值变大后却校验通过。例如 2000000000000001 仍在 JavaScript 安全整数范围内,Zod 4.6.7 的浮点余数容差却可能将它当成 2 的倍数。仅加一个安全整数检查,还没有消除这次误判。

先保留原始输入和实际解析后的 Number,区分两件事:输入是否早已超出可精确表示范围,以及校验器有没有正确判断一个仍可精确表示的整数。前者需要改变传输或数字表示,后者才是这里的 multipleOf 路径。

小数容差随数值放大,整数余数也被吞掉

4.6.7 的 Number 倍数检查使用 floatSafeRemainder。它先计算 value/step,再比较商与最近整数的差,容差按商的大小乘以 Number.EPSILON 扩大。

这有助于接受二进制浮点不能精确表示的十进制倍数,但大整数除以 2 得到的半整数,距离最近整数只有 0.5;容差足够大后,这个差也会被视为零。这里并不是字符串被 parseInt 读错,也不是这个示例整数本身已经丢了一位。

不要把边界简化成所有大于某个十进制数量级的数都错误。结果还取决于 step 和具体值。诊断可保留 2000000000000001、Number.MAX_SAFE_INTEGER 这些奇数,以及相邻合法偶数,分别检查是否符合业务约定。

业务只允许安全偶数时,明确检查整除

若契约是 JavaScript Number 能精确承接的整数,并且必须为偶数,可以在这个字段改用显式检查,不再叠加有问题的 multipleOf:

import * as z from 'zod';

const evenInteger = z.number().refine(
  value => Number.isSafeInteger(value) && value % 2 === 0,
  { message: '必须是安全范围内的偶数' },
);

这项规则同时拒绝非整数与超出安全整数范围的 Number。对于已经是安全整数的输入,整数除数 2 的余数判断不需要那项随商放大的小数容差。

这不是 multipleOf 的通用替代实现。若 step 是 0.01 等小数,直接 % 可能拒绝业务上应当接受的十进制结果;金额、计量精度等契约可采用受控的最小单位整数或十进制运算方案,但须同时限定输入范围和转换规则,不能随手乘一个系数后忽略溢出。

超出安全整数范围的编号或数量,应在输入边界保留完整文本,再按明确格式、长度和范围解析到所选的大整数类型。Zod 的 bigint 倍数检查走整数余数分支,但 JSON 并不会直接传入 bigint;先转成已经失真的 Number,再调用 BigInt,不会找回丢掉的数字。

不把通过校验等同于原始输入正确

修改后分别验证小奇数、大安全奇数、合法偶数、小数和超范围输入。奇数必须拒绝,合法整数按既定规则接受,错误输入仍进入原来的错误处理,不能在失败后返回原值。

如果数值来自字符串数组,还应检查map(parseInt) 的附加参数有没有先改变输入;那是转换阶段的另一种错误,不能通过调整倍数校验补救。

截至 2026 年 10 月 24 日,正式 4.6.7 的 Number 倍数判断仍使用上述容差。对这种安全整数契约,保留字段上的精确检查;以后采用修正依赖时,再用相同边界输入复核,而不是仅因升级了版本就撤掉业务约束。