Skip to content

Commit 07b0c3d

Browse files
authored
fix: preserve explicit superRefine issue input (#6053)
`_issue.input ??= payload.value` discarded an explicitly-supplied nullish `input`, so `superRefine` was the one place in Zod that could not emit an issue whose `input` matched the value-at-the-path convention every builtin issue follows. Forwarding a nested parse's issues was worse: an inner `input: undefined` for a missing key got overwritten with the outer payload, which breaks the documented `iss.input === undefined` test for a missing field. Switching to a presence check preserves an explicit `null` or `undefined` while still backfilling when `input` is omitted. `undefined` is a meaningful value for `input` in a way it isn't for the `code`/`inst`/`continue` defaults alongside it, which is why only this one moves off `??=`. Costs 26 B raw in classic, 0 B in mini unless `z.superRefine` is imported. No memory change, and the line only runs when an issue is constructed.
1 parent ba98071 commit 07b0c3d

4 files changed

Lines changed: 40 additions & 2 deletions

File tree

packages/zod/src/v4/classic/schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2199,7 +2199,7 @@ export const ZodTransform: core.$constructor<ZodTransform> = /*@__PURE__*/ core.
21992199

22002200
if (_issue.fatal) _issue.continue = false;
22012201
_issue.code ??= "custom";
2202-
_issue.input ??= payload.value;
2202+
if (!("input" in _issue)) _issue.input = payload.value;
22032203
_issue.inst ??= inst;
22042204
// _issue.continue ??= true;
22052205
payload.issues.push(util.issue(_issue));

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,26 @@ test("z.refine", () => {
735735
expect(() => z.parse(a, "hi")).toThrow();
736736
});
737737

738+
test("z.superRefine preserves explicit nullish issue input", () => {
739+
const schema = z.string().check(
740+
z.superRefine((_, ctx) => {
741+
ctx.addIssue({ code: "custom", message: "default" });
742+
ctx.addIssue({ code: "custom", message: "null", input: null });
743+
ctx.addIssue({ code: "custom", message: "undefined", input: undefined });
744+
})
745+
);
746+
747+
const result = z.safeParse(schema, "sensitive", { reportInput: true });
748+
expect(result.success).toEqual(false);
749+
if (!result.success) {
750+
expect(result.error.issues).toHaveLength(3);
751+
expect(result.error.issues[0].input).toEqual("sensitive");
752+
expect(result.error.issues[1].input).toEqual(null);
753+
expect(result.error.issues[2]).toHaveProperty("input");
754+
expect(result.error.issues[2].input).toEqual(undefined);
755+
}
756+
});
757+
738758
// test("z.superRefine", () => {
739759
// const a = z.number([
740760
// z.superRefine((val, ctx) => {

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,24 @@ describe("superRefine functionality", () => {
338338
}
339339
});
340340

341+
test("should preserve explicit nullish issue input", () => {
342+
const schema = z.string().superRefine((_, ctx) => {
343+
ctx.addIssue({ code: "custom", message: "default" });
344+
ctx.addIssue({ code: "custom", message: "null", input: null });
345+
ctx.addIssue({ code: "custom", message: "undefined", input: undefined });
346+
});
347+
348+
const result = schema.safeParse("sensitive", { reportInput: true });
349+
expect(result.success).toEqual(false);
350+
if (!result.success) {
351+
expect(result.error.issues).toHaveLength(3);
352+
expect(result.error.issues[0].input).toEqual("sensitive");
353+
expect(result.error.issues[1].input).toEqual(null);
354+
expect(result.error.issues[2]).toHaveProperty("input");
355+
expect(result.error.issues[2].input).toEqual(undefined);
356+
}
357+
});
358+
341359
test("should respect fatal flag in superRefine", () => {
342360
const schema = z
343361
.string()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1686,7 +1686,7 @@ export function _superRefine<T>(
16861686
const _issue: any = issue;
16871687
if (_issue.fatal) _issue.continue = false;
16881688
_issue.code ??= "custom";
1689-
_issue.input ??= payload.value;
1689+
if (!("input" in _issue)) _issue.input = payload.value;
16901690
_issue.inst ??= ch;
16911691
_issue.continue ??= !ch._zod.def.abort; // abort is always undefined, so this is always true...
16921692
payload.issues.push(util.issue(_issue));

0 commit comments

Comments
 (0)