diff --git a/deno/lib/__tests__/transformer.test.ts b/deno/lib/__tests__/transformer.test.ts index 0fe586aa7..c43f07896 100644 --- a/deno/lib/__tests__/transformer.test.ts +++ b/deno/lib/__tests__/transformer.test.ts @@ -279,6 +279,27 @@ test("z.NEVER in preprocess", async () => { } }); +// Fix for #3123 +test("preprocess only short circuit on its own issues", () => { + const schema1 = z.object({ + first: z.string(), + second: z.preprocess(() => undefined, z.string()), + }); + const schema2 = z.object({ + second: z.preprocess(() => undefined, z.string()), + first: z.string(), + }); + + const result1 = schema1.safeParse({}); + const result2 = schema2.safeParse({}); + + expect(result1.success).toEqual(false); + expect(result2.success).toEqual(false); + if (!result1.success && !result2.success) { + expect(result1.error.issues).toEqual(result2.error.issues.reverse()); + } +}); + test("short circuit on dirty", () => { const schema = z .string() diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 673ab1ec9..9e34ff35a 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -4264,8 +4264,10 @@ export class ZodEffects< const effect = this._def.effect || null; + let hasIssue = false; const checkCtx: RefinementCtx = { addIssue: (arg: IssueData) => { + hasIssue = true; addIssueToContext(ctx, arg); if (arg.fatal) { status.abort(); @@ -4282,7 +4284,7 @@ export class ZodEffects< if (effect.type === "preprocess") { const processed = effect.transform(ctx.data, checkCtx); - if (ctx.common.issues.length) { + if (hasIssue) { return { status: "dirty", value: ctx.data, diff --git a/src/__tests__/transformer.test.ts b/src/__tests__/transformer.test.ts index 5cf606c84..ceda464ad 100644 --- a/src/__tests__/transformer.test.ts +++ b/src/__tests__/transformer.test.ts @@ -278,6 +278,27 @@ test("z.NEVER in preprocess", async () => { } }); +// Fix for #3123 +test("preprocess only short circuit on its own issues", () => { + const schema1 = z.object({ + first: z.string(), + second: z.preprocess(() => undefined, z.string()), + }); + const schema2 = z.object({ + second: z.preprocess(() => undefined, z.string()), + first: z.string(), + }); + + const result1 = schema1.safeParse({}); + const result2 = schema2.safeParse({}); + + expect(result1.success).toEqual(false); + expect(result2.success).toEqual(false); + if (!result1.success && !result2.success) { + expect(result1.error.issues).toEqual(result2.error.issues.reverse()); + } +}); + test("short circuit on dirty", () => { const schema = z .string() diff --git a/src/types.ts b/src/types.ts index 567936b76..f8cbad3e7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4264,8 +4264,10 @@ export class ZodEffects< const effect = this._def.effect || null; + let hasIssue = false; const checkCtx: RefinementCtx = { addIssue: (arg: IssueData) => { + hasIssue = true; addIssueToContext(ctx, arg); if (arg.fatal) { status.abort(); @@ -4282,7 +4284,7 @@ export class ZodEffects< if (effect.type === "preprocess") { const processed = effect.transform(ctx.data, checkCtx); - if (ctx.common.issues.length) { + if (hasIssue) { return { status: "dirty", value: ctx.data,