Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/docs/content/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
34 changes: 33 additions & 1 deletion packages/zod/src/v4/classic/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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>;
Expand Down
5 changes: 5 additions & 0 deletions packages/zod/src/v4/classic/tests/firstparty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ test("first party switch", () => {
break;
case "default":
break;
case "prefault":
break;
case "template_literal":
break;
case "custom":
Expand Down Expand Up @@ -146,6 +148,8 @@ test("$ZodSchemaTypes", () => {
break;
case "default":
break;
case "prefault":
break;
case "template_literal":
break;
case "custom":
Expand All @@ -166,6 +170,7 @@ test("$ZodSchemaTypes", () => {
break;
case "lazy":
break;

default:
expectTypeOf(type).toEqualTypeOf<never>();
}
Expand Down
14 changes: 14 additions & 0 deletions packages/zod/src/v4/classic/tests/json-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
{
Expand All @@ -1382,6 +1383,12 @@ test("input type", () => {
},
],
},
"e": {
"default": {
"value": "hello",
},
"type": "string",
},
},
"required": [
"a",
Expand Down Expand Up @@ -1413,11 +1420,18 @@ test("input type", () => {
},
],
},
"e": {
"default": {
"value": "hello",
},
"type": "string",
},
},
"required": [
"a",
"c",
"d",
"e",
],
"type": "object",
}
Expand Down
37 changes: 37 additions & 0 deletions packages/zod/src/v4/classic/tests/prefault.test.ts
Original file line number Diff line number Diff line change
@@ -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;
}>();
});
4 changes: 3 additions & 1 deletion packages/zod/src/v4/core/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
60 changes: 52 additions & 8 deletions packages/zod/src/v4/core/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface $ZodTypeDef {
| "success"
| "transform"
| "default"
| "prefault"
| "catch"
| "nan"
| "pipe"
Expand Down Expand Up @@ -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";
Expand All @@ -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. */
Expand All @@ -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);
};
}
);

///////////////////////////////////////////////
///////////////////////////////////////////////
////////// //////////
Expand Down Expand Up @@ -3634,6 +3677,7 @@ export type $ZodTypes =
| $ZodLazy
| $ZodOptional
| $ZodDefault
| $ZodPrefault
| $ZodTemplateLiteral
| $ZodCustom
| $ZodTransform
Expand Down
8 changes: 7 additions & 1 deletion packages/zod/src/v4/core/to-json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
30 changes: 28 additions & 2 deletions packages/zod/src/v4/mini/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 0 additions & 5 deletions play.ts
Original file line number Diff line number Diff line change
@@ -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);