Skip to content

Commit 97edaf7

Browse files
fix(v4): don't throw from safeParse on bigint multipleOf(0n) (#6466)
A bigint schema with multipleOf(0n) threw `RangeError: Division by zero` out of safeParse. The runtime check now guards the divisor, matching the number branch, which already reports a failure instead of throwing. The compiler emitted `x % 0n !== 0n`, so a compiled schema still threw. A zero divisor is now declined at compile time and the node becomes a runtime island, the same way the compiler already declines a NaN comparison bound. Supersedes #6126 and #6140. Co-authored-by: MerlijnW70 <196433316+MerlijnW70@users.noreply.github.com>
1 parent 21a6f0c commit 97edaf7

4 files changed

Lines changed: 24 additions & 1 deletion

File tree

packages/zod/src/v4/classic/tests/bigint.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,21 @@ test("bigint formats are distinct at the type level", () => {
6363
// @ts-expect-error a uint64 schema is not a ZodInt64
6464
z.uint64() satisfies z.ZodInt64;
6565
});
66+
67+
test("multipleOf(0n) does not throw from safeParse", () => {
68+
// `value % 0n` throws RangeError, so the compiled path declines and the runtime reports the failure
69+
const schema = z.bigint().multipleOf(BigInt(0));
70+
const result = schema.safeParse(BigInt(10));
71+
expect(result.success).toBe(false);
72+
expect(result.error!.issues[0].code).toEqual("not_multiple_of");
73+
expect(schema.safeParse(BigInt(0)).success).toBe(false);
74+
75+
// matches the number equivalent
76+
expect(z.number().multipleOf(0).safeParse(10).success).toBe(false);
77+
expect(z.number().multipleOf(0).safeParse(0).success).toBe(false);
78+
79+
// a zero divisor nested in an object must not break the surrounding parse
80+
const obj = z.object({ a: z.bigint().multipleOf(BigInt(0)), b: z.bigint() });
81+
expect(obj.safeParse({ a: BigInt(1), b: BigInt(2) }).success).toBe(false);
82+
expect(obj.safeParse({ a: BigInt(1), b: 2 }).success).toBe(false);
83+
});

packages/zod/src/v4/core/checks.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ export const $ZodCheckMultipleOf: core.$constructor<$ZodCheckMultipleOf<number |
190190
throw new Error("Cannot mix number and bigint in multiple_of check.");
191191
const isMultiple =
192192
typeof payload.value === "bigint"
193-
? payload.value % (def.value as bigint) === BigInt(0)
193+
? // `value % 0n` throws, and nothing is a multiple of zero — the number branch already fails this way via NaN
194+
(def.value as bigint) !== BigInt(0) && payload.value % (def.value as bigint) === BigInt(0)
194195
: util.floatSafeRemainder(payload.value, def.value as number) === 0;
195196

196197
if (isMultiple) return;

packages/zod/src/v4/core/compile.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ function generateMultipleOfCheck(
460460
accessor: string
461461
): void {
462462
if (typeof def.value === "bigint") {
463+
// a zero divisor has no compiled form: `x % 0n` throws
464+
if (def.value === BigInt(0)) throw new ZodCompileUnsupportedError("multiple_of check with a zero divisor");
463465
doc.write(`if (${accessor} % ${def.value}n !== 0n) return INVALID;`);
464466
} else {
465467
// Float `%` has well-known precision issues for sub-integer steps

packages/zod/src/v4/core/tests/compile.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,8 @@ test("unsupported features throw ZodCompileUnsupportedError, not raw errors", ()
14821482
);
14831483
// NaN comparison bounds can't compile to a faithful comparison.
14841484
expect(() => compile(z.number().gt(Number.NaN))).toThrow(ZodCompileUnsupportedError);
1485+
// A zero bigint divisor has no compiled form: `x % 0n` throws.
1486+
expect(() => compile(z.bigint().multipleOf(BigInt(0)))).toThrow(ZodCompileUnsupportedError);
14851487
// Unsupported children still island inside containers.
14861488
const aot = compile(z.object({ a: z.string(), x: z.xor([z.literal("p"), z.literal("q")]) }));
14871489
expect(valid(aot, { a: "x", x: "p" })).toEqual({ a: "x", x: "p" });

0 commit comments

Comments
 (0)