Repository: colinhacks/zod
Pull request #4552 (2025-05-28 22:13:36)

Use output type for preprocess
Closes https://github.com/colinhacks/zod/issues/4548


Patch:
diff --git a/packages/zod/src/v3/types.ts b/packages/zod/src/v3/types.ts
index 2b3efe02..1df681cb 100644
--- a/packages/zod/src/v3/types.ts
+++ b/packages/zod/src/v3/types.ts
@@ -4074,7 +4074,7 @@ function createZodEnum(values: [string, ...string[]], params?: RawCreateParams)
 }
 
 export class ZodEnum<T extends [string, ...string[]]> extends ZodType<T[number], ZodEnumDef<T>, T[number]> {
-  #cache: Set<T[number]> | undefined;
+  _cache: Set<T[number]> | undefined;
 
   _parse(input: ParseInput): ParseReturnType<this["_output"]> {
     if (typeof input.data !== "string") {
@@ -4088,11 +4088,11 @@ export class ZodEnum<T extends [string, ...string[]]> extends ZodType<T[number],
       return INVALID;
     }
 
-    if (!this.#cache) {
-      this.#cache = new Set(this._def.values);
+    if (!this._cache) {
+      this._cache = new Set(this._def.values);
     }
 
-    if (!this.#cache.has(input.data)) {
+    if (!this._cache.has(input.data)) {
       const ctx = this._getOrReturnCtx(input);
       const expectedValues = this._def.values;
 
@@ -4172,7 +4172,7 @@ export interface ZodNativeEnumDef<T extends EnumLike = EnumLike> extends ZodType
 export type EnumLike = { [k: string]: string | number; [nu: number]: string };
 
 export class ZodNativeEnum<T extends EnumLike> extends ZodType<T[keyof T], ZodNativeEnumDef<T>, T[keyof T]> {
-  #cache: Set<T[keyof T]> | undefined;
+  _cache: Set<T[keyof T]> | undefined;
   _parse(input: ParseInput): ParseReturnType<T[keyof T]> {
     const nativeEnumValues = util.getValidEnumValues(this._def.values);
 
@@ -4187,11 +4187,11 @@ export class ZodNativeEnum<T extends EnumLike> extends ZodType<T[keyof T], ZodNa
       return INVALID;
     }
 
-    if (!this.#cache) {
-      this.#cache = new Set(util.getValidEnumValues(this._def.values));
+    if (!this._cache) {
+      this._cache = new Set(util.getValidEnumValues(this._def.values));
     }
 
-    if (!this.#cache.has(input.data)) {
+    if (!this._cache.has(input.data)) {
       const expectedValues = util.objectValues(nativeEnumValues);
 
       addIssueToContext(ctx, {
@@ -4411,7 +4411,7 @@ export class ZodEffects<T extends ZodTypeAny, Output = output<T>, Input = input<
           parent: ctx,
         });
 
-        if (!isValid(base)) return base;
+        if (!isValid(base)) return INVALID;
 
         const result = effect.transform(base.value, checkCtx);
         if (result instanceof Promise) {
@@ -4423,7 +4423,7 @@ export class ZodEffects<T extends ZodTypeAny, Output = output<T>, Input = input<
         return { status: status.value, value: result };
       } else {
         return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => {
-          if (!isValid(base)) return base;
+          if (!isValid(base)) return INVALID;
 
           return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({
             status: status.value,
diff --git a/packages/zod/src/v4/core/to-json-schema.ts b/packages/zod/src/v4/core/to-json-schema.ts
index 06eeb793..641c7943 100644
--- a/packages/zod/src/v4/core/to-json-schema.ts
+++ b/packages/zod/src/v4/core/to-json-schema.ts
@@ -499,7 +499,7 @@ export class JSONSchemaGenerator {
           break;
         }
         case "pipe": {
-          const innerType = this.io === "input" ? def.in : def.out;
+          const innerType = this.io === "input" ? (def.in._zod.def.type === "transform" ? def.out : def.in) : def.out;
           this.process(innerType, params);
           result.ref = innerType;
           break;
diff --git a/play.ts b/play.ts
index 41eb006b..23bdd10a 100644
--- a/play.ts
+++ b/play.ts
@@ -1,3 +1,21 @@
-import { z } from "zod/v4";
+import { z } from "zod/v3";
 
-z;
+// import * as z from "zod";
+
+const parseResult = z
+  .string()
+  .refine((x) => false) // force anything/everything to fail
+  .transform((x) => {
+    console.log("I don't get called"); // correct
+    return x.length;
+  })
+  .refine((x) => {
+    console.log("I shouldn't get called!!!!!!");
+    console.log(typeof x); // number
+    console.log(x);
+  })
+  .safeParse("123");
+
+console.log(`succeeded:  ${parseResult.success}`); // false (correct behavior)
+
+// z.string().min(1234, { abort: true });


Test patch:
diff --git a/packages/zod/src/v3/tests/refine.test.ts b/packages/zod/src/v3/tests/refine.test.ts
index 8a7165cd..55c27fea 100644
--- a/packages/zod/src/v3/tests/refine.test.ts
+++ b/packages/zod/src/v3/tests/refine.test.ts
@@ -295,3 +295,19 @@ test("fatal superRefine", () => {
   expect(result.success).toEqual(false);
   if (!result.success) expect(result.error.issues.length).toEqual(1);
 });
+
+test("superRefine after skipped transform", () => {
+  const schema = z
+    .string()
+    .regex(/^\d+$/)
+    .transform((val) => Number(val))
+    .superRefine((val) => {
+      if (typeof val !== "number") {
+        throw new Error("Called without transform");
+      }
+    });
+
+  const result = schema.safeParse("");
+
+  expect(result.success).toEqual(false);
+});
diff --git a/packages/zod/src/v4/classic/tests/json-schema.test.ts b/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
similarity index 99%
rename from packages/zod/src/v4/classic/tests/json-schema.test.ts
rename to packages/zod/src/v4/classic/tests/to-json-schema.test.ts
index d014ddf2..9aeac330 100644
--- a/packages/zod/src/v4/classic/tests/json-schema.test.ts
+++ b/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
@@ -1943,3 +1943,14 @@ test("examples on pipe", () => {
 //   `);
 
 // });
+
+test("use output type for preprocess", () => {
+  const a = z.preprocess((val) => String(val), z.string());
+
+  expect(z.toJSONSchema(a, { io: "input" })).toMatchInlineSnapshot(`
+    {
+      "$schema": "https://json-schema.org/draft/2020-12/schema",
+      "type": "string",
+    }
+  `);
+});