Skip to content

Commit 680d19f

Browse files
colinhacksclaude
andauthored
fix(v4): report a union option that only checks rejected (#6662)
Since #6652 an oversized array, set or map fails its size bound before its elements are parsed, and the issue aborts so nothing reads the unparsed value. A union treated that abort like a type mismatch and fell back to invalid_union, where 4.6.5 reported the option's too_big. When no option can continue, the union now falls back to the options whose issues all came from checks, and reports that option if it is the only one. Fixes #6659 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 40a5dba commit 680d19f

2 files changed

Lines changed: 19 additions & 4 deletions

File tree

‎packages/zod/src/v4/classic/tests/union.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ test("surface continuable errors only if they exist", () => {
180180
`);
181181
});
182182

183+
test("surface an option a check stopped when none can continue", () => {
184+
// the size cap aborts before parsing elements
185+
const schema = z.union([z.array(z.string()).max(2), z.string()]);
186+
expect(schema.safeParse(["a", "b", "c"]).error!.issues).toMatchObject([
187+
{ code: "too_big", origin: "array", path: [] },
188+
]);
189+
expect(z.union([z.int(), z.number().max(5)]).safeParse(7.5).error!.issues).toMatchObject([{ code: "too_big" }]);
190+
const both = z.union([z.array(z.string()).max(2), z.array(z.number()).max(1)]);
191+
expect(both.safeParse(["a", "b", "c"]).error!.issues).toMatchObject([{ code: "invalid_union" }]);
192+
});
193+
183194
// z.xor() tests
184195
test("z.xor() - exactly one match succeeds", () => {
185196
const schema = z.xor([z.string(), z.number()]);

‎packages/zod/src/v4/core/schemas.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2466,10 +2466,14 @@ function handleUnionResults(results: ParsePayload[], final: ParsePayload, inst:
24662466
}
24672467
}
24682468

2469-
const nonaborted = results.filter((r) => !util.aborted(r));
2470-
if (nonaborted.length === 1) {
2471-
final.value = nonaborted[0].value;
2472-
return nonaborted[0];
2469+
let matched = results.filter((r) => !util.aborted(r));
2470+
// failing that, an option only checks rejected still matched the input's type
2471+
if (!matched.length) {
2472+
matched = results.filter((r) => !r.aborted && r.issues.every((iss) => iss.continue !== undefined));
2473+
}
2474+
if (matched.length === 1) {
2475+
final.value = matched[0].value;
2476+
return matched[0];
24732477
}
24742478

24752479
final.issues.push({

0 commit comments

Comments
 (0)