Repository: colinhacks/zod
Pull request #4376 (2025-05-15 20:39:38)

Implement `z.prefault`
Zod 4 changes the default (ha) behavior of `.default()` such that it short-circuits the parsing process. If the input is `undefined`, the default value is eagerly returned. As such, the default value must be assignable to the *output type* of the schema.

```ts
const schema = z.string().transform(val => val.length).default(0);
schema.parse(undefined); // => 0
```

In Zod 3, it behaved differently. If the parse input was `undefined`, the default value would be parsed instead. 

The behavior remains available in Zod 4 via the `.prefault()` API (short for "pre-parse default"). Unlike `.default()`, `.prefault()` does not short circuit the parsing. As such, the prefault value must be assignable to the *input type* of the schema:

```ts
z.string().transform(val => val.length).prefault("tuna");
schema.parse(undefined); // => 4
```



coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=colinhacks/zod&utm_content=4376):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Patch:
diff --git a/packages/docs/content/api.mdx b/packages/docs/content/api.mdx
index 0de29ab2..7072d53b 100644
--- a/packages/docs/content/api.mdx
+++ b/packages/docs/content/api.mdx
@@ -2064,6 +2064,37 @@ z.parse(randomDefault, undefined); // => 0.7223408162401552
 </Tab>
 </Tabs>
 
+
+<Accordions>
+<Accordion title="Prefaults">
+
+
+  In Zod, setting a *default* value will short-circuit the parsing process. If the input is `undefined`, the default value is eagerly returned. As such, the default value must be assignable to the *output type* of the schema.
+
+  ```ts
+  const schema = z.string().transform(val => val.length).default(0);
+  schema.parse(undefined); // => 0
+  ```
+  
+  Sometimes, it's useful to define a "prefault" value. If the input is `undefined`, this value will be parsed instead. The parsing process is *not* short circuited. As such, the prefault value must be assignable to the *input type* of the schema.
+
+  ```ts
+  z.string().transform(val => val.length).prefault("tuna");
+  schema.parse(undefined); // => 4
+  ```
+
+  This is also useful if you want to pass some input value through some mutating refinements.
+
+  ```ts
+  const a = z.string().trim().toUpperCase().prefault("  tuna  ");
+  a.parse(undefined); // => "TUNA"
+
+  const b = z.string().trim().toUpperCase().default("  tuna  ");
+  b.parse(undefined); // => "  tuna  "
+  ```
+</Accordion>
+</Accordions>
+
 ## Catch
 
 Use `.catch()` to define a fallback value to be returned in the event of a validation error:
diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts
index b1e05b2e..cd4b9bd4 100644
--- a/packages/zod/src/v4/classic/schemas.ts
+++ b/packages/zod/src/v4/classic/schemas.ts
@@ -70,6 +70,8 @@ export interface ZodType<out Output = unknown, out Input = unknown> extends core
   nullish(): ZodOptional<ZodNullable<this>>;
   default(def: util.NoUndefined<core.output<this>>): ZodDefault<this>;
   default(def: () => util.NoUndefined<core.output<this>>): ZodDefault<this>;
+  prefault(def: () => core.input<this>): ZodPrefault<this>;
+  prefault(def: core.input<this>): ZodPrefault<this>;
   array(): ZodArray<this>;
   or<T extends core.$ZodType>(option: T): ZodUnion<[this, T]>;
   and<T extends core.$ZodType>(incoming: T): ZodIntersection<this, T>;
@@ -166,6 +168,7 @@ export const ZodType: core.$constructor<ZodType> = /*@__PURE__*/ core.$construct
   inst.and = (arg) => intersection(inst, arg);
   inst.transform = (tx) => pipe(inst, transform(tx as any)) as never;
   inst.default = (def) => _default(inst, def);
+  inst.prefault = (def) => prefault(inst, def);
   // inst.coalesce = (def, params) => coalesce(inst, def, params);
   inst.catch = (params) => _catch(inst, params);
   inst.pipe = (target) => pipe(inst, target);
@@ -1698,11 +1701,40 @@ export function _default<T extends core.$ZodType>(
 ): ZodDefault<T> {
   return new ZodDefault({
     type: "default",
-    defaultValue: (typeof defaultValue === "function" ? defaultValue : () => defaultValue) as () => core.output<T>,
     innerType,
+    get defaultValue() {
+      return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue;
+    },
   }) as any as ZodDefault<T>;
 }
 
+// ZodPrefault
+export interface ZodPrefault<T extends core.$ZodType = core.$ZodType> extends ZodType {
+  _zod: core.$ZodPrefaultInternals<T>;
+  unwrap(): T;
+}
+export const ZodPrefault: core.$constructor<ZodPrefault> = /*@__PURE__*/ core.$constructor(
+  "ZodPrefault",
+  (inst, def) => {
+    core.$ZodPrefault.init(inst, def);
+    ZodType.init(inst, def);
+    inst.unwrap = () => inst._zod.def.innerType;
+  }
+);
+
+export function prefault<T extends core.$ZodType>(
+  innerType: T,
+  defaultValue: core.input<T> | (() => core.input<T>)
+): ZodPrefault<T> {
+  return new ZodPrefault({
+    type: "prefault",
+    innerType,
+    get defaultValue() {
+      return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue;
+    },
+  }) as ZodPrefault<T>;
+}
+
 // ZodNonOptional
 export interface ZodNonOptional<T extends core.$ZodType = core.$ZodType> extends ZodType {
   _zod: core.$ZodNonOptionalInternals<T>;
diff --git a/packages/zod/src/v4/core/api.ts b/packages/zod/src/v4/core/api.ts
index 996beb97..df59752b 100644
--- a/packages/zod/src/v4/core/api.ts
+++ b/packages/zod/src/v4/core/api.ts
@@ -1260,8 +1260,10 @@ export function _default<T extends schemas.$ZodObject>(
 ): schemas.$ZodDefault<T> {
   return new Class({
     type: "default",
-    defaultValue: (typeof defaultValue === "function" ? defaultValue : () => defaultValue) as any,
     innerType,
+    get defaultValue() {
+      return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue;
+    },
   }) as any;
 }
 
diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts
index c036c773..19754c2e 100644
--- a/packages/zod/src/v4/core/schemas.ts
+++ b/packages/zod/src/v4/core/schemas.ts
@@ -67,6 +67,7 @@ export interface $ZodTypeDef {
     | "success"
     | "transform"
     | "default"
+    | "prefault"
     | "catch"
     | "nan"
     | "pipe"
@@ -2975,15 +2976,12 @@ export const $ZodNullable: core.$constructor<$ZodNullable> = /*@__PURE__*/ core.
 export interface $ZodDefaultDef<T extends $ZodType = $ZodType> extends $ZodTypeDef {
   type: "default";
   innerType: T;
-  defaultValue: () => util.NoUndefined<core.output<T>>;
+  /** The default value. May be a getter. */
+  defaultValue: util.NoUndefined<core.output<T>>;
 }
 
 export interface $ZodDefaultInternals<T extends $ZodType = $ZodType>
-  extends $ZodTypeInternals<
-    // this is pragmatic but not strictly correct
-    util.NoUndefined<core.output<T>>,
-    core.input<T> | undefined
-  > {
+  extends $ZodTypeInternals<util.NoUndefined<core.output<T>>, core.input<T> | undefined> {
   def: $ZodDefaultDef<T>;
   // qin: "true";
   optionality: "defaulted";
@@ -3006,7 +3004,7 @@ export const $ZodDefault: core.$constructor<$ZodDefault> = /*@__PURE__*/ core.$c
 
     inst._zod.parse = (payload, ctx) => {
       if (payload.value === undefined) {
-        payload.value = def.defaultValue();
+        payload.value = def.defaultValue;
         /**
          * $ZodDefault always returns the default value immediately.
          * It doesn't pass the default value into the validator ("prefault"). There's no reason to pass the default value through validation. The validity of the default is enforced by TypeScript statically. Otherwise, it's the responsibility of the user to ensure the default is valid. In the case of pipes with divergent in/out types, you can specify the default on the `in` schema of your ZodPipe to set a "prefault" for the pipe.   */
@@ -3023,10 +3021,55 @@ export const $ZodDefault: core.$constructor<$ZodDefault> = /*@__PURE__*/ core.$c
 
 function handleDefaultResult(payload: ParsePayload, def: $ZodDefaultDef) {
   if (payload.value === undefined) {
-    payload.value = def.defaultValue();
+    payload.value = def.defaultValue;
   }
   return payload;
 }
+
+////////////////////////////////////////////
+////////////////////////////////////////////
+//////////                        //////////
+//////////      $ZodPrefault      //////////
+//////////                        //////////
+////////////////////////////////////////////
+////////////////////////////////////////////
+
+export interface $ZodPrefaultDef<T extends $ZodType = $ZodType> extends $ZodTypeDef {
+  type: "prefault";
+  innerType: T;
+  /** The default value. May be a getter. */
+  defaultValue: core.input<T>;
+}
+
+export interface $ZodPrefaultInternals<T extends $ZodType = $ZodType>
+  extends $ZodTypeInternals<util.NoUndefined<core.output<T>>, core.input<T> | undefined> {
+  def: $ZodPrefaultDef<T>;
+  optionality: "defaulted";
+  isst: never;
+  values: T["_zod"]["values"];
+}
+
+export interface $ZodPrefault<T extends $ZodType = $ZodType> extends $ZodType {
+  _zod: $ZodPrefaultInternals<T>;
+}
+
+export const $ZodPrefault: core.$constructor<$ZodPrefault> = /*@__PURE__*/ core.$constructor(
+  "$ZodPrefault",
+  (inst, def) => {
+    $ZodType.init(inst, def);
+
+    inst._zod.optionality = "defaulted";
+    inst._zod.values = def.innerType._zod.values;
+
+    inst._zod.parse = (payload, ctx) => {
+      if (payload.value === undefined) {
+        payload.value = def.defaultValue;
+      }
+      return def.innerType._zod.run(payload, ctx);
+    };
+  }
+);
+
 ///////////////////////////////////////////////
 ///////////////////////////////////////////////
 //////////                           //////////
@@ -3634,6 +3677,7 @@ export type $ZodTypes =
   | $ZodLazy
   | $ZodOptional
   | $ZodDefault
+  | $ZodPrefault
   | $ZodTemplateLiteral
   | $ZodCustom
   | $ZodTransform
diff --git a/packages/zod/src/v4/core/to-json-schema.ts b/packages/zod/src/v4/core/to-json-schema.ts
index abd4d697..19f2e919 100644
--- a/packages/zod/src/v4/core/to-json-schema.ts
+++ b/packages/zod/src/v4/core/to-json-schema.ts
@@ -467,7 +467,13 @@ export class JSONSchemaGenerator {
         case "default": {
           const inner = this.process(def.innerType, params);
           Object.assign(_json, inner);
-          _json.default = def.defaultValue();
+          _json.default = def.defaultValue;
+          break;
+        }
+        case "prefault": {
+          const inner = this.process(def.innerType, params);
+          Object.assign(_json, inner);
+          _json.default = schema["~standard"].validate(undefined);
           break;
         }
         case "catch": {
diff --git a/packages/zod/src/v4/mini/schemas.ts b/packages/zod/src/v4/mini/schemas.ts
index 66be2862..5cfa4805 100644
--- a/packages/zod/src/v4/mini/schemas.ts
+++ b/packages/zod/src/v4/mini/schemas.ts
@@ -1225,9 +1225,35 @@ export function _default<T extends SomeType>(
 ): ZodMiniDefault<T> {
   return new ZodMiniDefault({
     type: "default",
-    defaultValue: (typeof defaultValue === "function" ? defaultValue : () => defaultValue) as () => core.output<T>,
     innerType,
-  }) as any as ZodMiniDefault<T>;
+    get defaultValue() {
+      return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue;
+    },
+  }) as ZodMiniDefault<T>;
+}
+
+// ZodMiniPrefault
+export interface ZodMiniPrefault<T extends SomeType = SomeType> extends ZodMiniType {
+  _zod: core.$ZodPrefaultInternals<T>;
+}
+export const ZodMiniPrefault: core.$constructor<ZodMiniPrefault> = /*@__PURE__*/ core.$constructor(
+  "ZodMiniPrefault",
+  (inst, def) => {
+    core.$ZodPrefault.init(inst, def);
+    ZodMiniType.init(inst, def);
+  }
+);
+export function prefault<T extends SomeType>(
+  innerType: T,
+  defaultValue: util.NoUndefined<core.input<T>> | (() => util.NoUndefined<core.input<T>>)
+): ZodMiniPrefault<T> {
+  return new ZodMiniPrefault({
+    type: "prefault",
+    innerType,
+    get defaultValue() {
+      return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue;
+    },
+  }) as ZodMiniPrefault<T>;
 }
 
 // ZodMiniNonOptional
diff --git a/play.ts b/play.ts
index 82831bb2..e69de29b 100644
--- a/play.ts
+++ b/play.ts
@@ -1,5 +0,0 @@
-import * as z from "zod/v4";
-
-// z.string().check(z.startsWith("asdf", "bad")).parse("qwer");
-
-console.log(z.string().includes("Error")._zod.def);


Test patch:
diff --git a/packages/zod/src/v4/classic/tests/firstparty.test.ts b/packages/zod/src/v4/classic/tests/firstparty.test.ts
index 31912948..ac689930 100644
--- a/packages/zod/src/v4/classic/tests/firstparty.test.ts
+++ b/packages/zod/src/v4/classic/tests/firstparty.test.ts
@@ -60,6 +60,8 @@ test("first party switch", () => {
       break;
     case "default":
       break;
+    case "prefault":
+      break;
     case "template_literal":
       break;
     case "custom":
@@ -146,6 +148,8 @@ test("$ZodSchemaTypes", () => {
       break;
     case "default":
       break;
+    case "prefault":
+      break;
     case "template_literal":
       break;
     case "custom":
@@ -166,6 +170,7 @@ test("$ZodSchemaTypes", () => {
       break;
     case "lazy":
       break;
+
     default:
       expectTypeOf(type).toEqualTypeOf<never>();
   }
diff --git a/packages/zod/src/v4/classic/tests/json-schema.test.ts b/packages/zod/src/v4/classic/tests/json-schema.test.ts
index e2faabf5..df6ad2ea 100644
--- a/packages/zod/src/v4/classic/tests/json-schema.test.ts
+++ b/packages/zod/src/v4/classic/tests/json-schema.test.ts
@@ -1358,6 +1358,7 @@ test("input type", () => {
     b: z.string().optional(),
     c: z.string().default("hello"),
     d: z.string().nullable(),
+    e: z.string().prefault("hello"),
   });
   expect(toJSONSchema(schema, { io: "input" })).toMatchInlineSnapshot(`
     {
@@ -1382,6 +1383,12 @@ test("input type", () => {
             },
           ],
         },
+        "e": {
+          "default": {
+            "value": "hello",
+          },
+          "type": "string",
+        },
       },
       "required": [
         "a",
@@ -1413,11 +1420,18 @@ test("input type", () => {
             },
           ],
         },
+        "e": {
+          "default": {
+            "value": "hello",
+          },
+          "type": "string",
+        },
       },
       "required": [
         "a",
         "c",
         "d",
+        "e",
       ],
       "type": "object",
     }
diff --git a/packages/zod/src/v4/classic/tests/prefault.test.ts b/packages/zod/src/v4/classic/tests/prefault.test.ts
new file mode 100644
index 00000000..72178ab6
--- /dev/null
+++ b/packages/zod/src/v4/classic/tests/prefault.test.ts
@@ -0,0 +1,37 @@
+import { expect, expectTypeOf, test } from "vitest";
+import { z } from "zod/v4";
+
+test("basic prefault", () => {
+  const a = z.prefault(z.string().trim(), "  default  ");
+  expect(a).toBeInstanceOf(z.ZodPrefault);
+  expect(a.parse("  asdf  ")).toEqual("asdf");
+  expect(a.parse(undefined)).toEqual("default");
+
+  type inp = z.input<typeof a>;
+  expectTypeOf<inp>().toEqualTypeOf<string | undefined>();
+  type out = z.output<typeof a>;
+  expectTypeOf<out>().toEqualTypeOf<string>();
+});
+
+test("prefault inside object", () => {
+  // test optinality
+  const a = z.object({
+    name: z.string().optional(),
+    age: z.number().default(1234),
+    email: z.string().prefault("1234"),
+  });
+
+  type inp = z.input<typeof a>;
+  expectTypeOf<inp>().toEqualTypeOf<{
+    name?: string | undefined;
+    age?: number | undefined;
+    email?: string | undefined;
+  }>();
+
+  type out = z.output<typeof a>;
+  expectTypeOf<out>().toEqualTypeOf<{
+    name?: string | undefined;
+    age: number;
+    email: string;
+  }>();
+});