diff --git a/packages/docs/content/api.mdx b/packages/docs/content/api.mdx index 578df4e598..8503cd30ac 100644 --- a/packages/docs/content/api.mdx +++ b/packages/docs/content/api.mdx @@ -2064,6 +2064,37 @@ z.parse(randomDefault, undefined); // => 0.7223408162401552 + + + + + + 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 " + ``` + + + ## 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 b1e05b2e74..cd4b9bd4db 100644 --- a/packages/zod/src/v4/classic/schemas.ts +++ b/packages/zod/src/v4/classic/schemas.ts @@ -70,6 +70,8 @@ export interface ZodType extends core nullish(): ZodOptional>; default(def: util.NoUndefined>): ZodDefault; default(def: () => util.NoUndefined>): ZodDefault; + prefault(def: () => core.input): ZodPrefault; + prefault(def: core.input): ZodPrefault; array(): ZodArray; or(option: T): ZodUnion<[this, T]>; and(incoming: T): ZodIntersection; @@ -166,6 +168,7 @@ export const ZodType: core.$constructor = /*@__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( ): ZodDefault { return new ZodDefault({ type: "default", - defaultValue: (typeof defaultValue === "function" ? defaultValue : () => defaultValue) as () => core.output, innerType, + get defaultValue() { + return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue; + }, }) as any as ZodDefault; } +// ZodPrefault +export interface ZodPrefault extends ZodType { + _zod: core.$ZodPrefaultInternals; + unwrap(): T; +} +export const ZodPrefault: core.$constructor = /*@__PURE__*/ core.$constructor( + "ZodPrefault", + (inst, def) => { + core.$ZodPrefault.init(inst, def); + ZodType.init(inst, def); + inst.unwrap = () => inst._zod.def.innerType; + } +); + +export function prefault( + innerType: T, + defaultValue: core.input | (() => core.input) +): ZodPrefault { + return new ZodPrefault({ + type: "prefault", + innerType, + get defaultValue() { + return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue; + }, + }) as ZodPrefault; +} + // ZodNonOptional export interface ZodNonOptional extends ZodType { _zod: core.$ZodNonOptionalInternals; diff --git a/packages/zod/src/v4/classic/tests/firstparty.test.ts b/packages/zod/src/v4/classic/tests/firstparty.test.ts index 3191294826..ac6899309b 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(); } 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 e2faabf5ee..df6ad2ead3 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 0000000000..72178ab603 --- /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; + expectTypeOf().toEqualTypeOf(); + type out = z.output; + expectTypeOf().toEqualTypeOf(); +}); + +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; + expectTypeOf().toEqualTypeOf<{ + name?: string | undefined; + age?: number | undefined; + email?: string | undefined; + }>(); + + type out = z.output; + expectTypeOf().toEqualTypeOf<{ + name?: string | undefined; + age: number; + email: string; + }>(); +}); diff --git a/packages/zod/src/v4/core/api.ts b/packages/zod/src/v4/core/api.ts index 996beb976e..df59752bd0 100644 --- a/packages/zod/src/v4/core/api.ts +++ b/packages/zod/src/v4/core/api.ts @@ -1260,8 +1260,10 @@ export function _default( ): schemas.$ZodDefault { 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 c036c77358..19754c2ec7 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 extends $ZodTypeDef { type: "default"; innerType: T; - defaultValue: () => util.NoUndefined>; + /** The default value. May be a getter. */ + defaultValue: util.NoUndefined>; } export interface $ZodDefaultInternals - extends $ZodTypeInternals< - // this is pragmatic but not strictly correct - util.NoUndefined>, - core.input | undefined - > { + extends $ZodTypeInternals>, core.input | undefined> { def: $ZodDefaultDef; // 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 extends $ZodTypeDef { + type: "prefault"; + innerType: T; + /** The default value. May be a getter. */ + defaultValue: core.input; +} + +export interface $ZodPrefaultInternals + extends $ZodTypeInternals>, core.input | undefined> { + def: $ZodPrefaultDef; + optionality: "defaulted"; + isst: never; + values: T["_zod"]["values"]; +} + +export interface $ZodPrefault extends $ZodType { + _zod: $ZodPrefaultInternals; +} + +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 abd4d69703..19f2e91949 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 66be28628b..5cfa4805ec 100644 --- a/packages/zod/src/v4/mini/schemas.ts +++ b/packages/zod/src/v4/mini/schemas.ts @@ -1225,9 +1225,35 @@ export function _default( ): ZodMiniDefault { return new ZodMiniDefault({ type: "default", - defaultValue: (typeof defaultValue === "function" ? defaultValue : () => defaultValue) as () => core.output, innerType, - }) as any as ZodMiniDefault; + get defaultValue() { + return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue; + }, + }) as ZodMiniDefault; +} + +// ZodMiniPrefault +export interface ZodMiniPrefault extends ZodMiniType { + _zod: core.$ZodPrefaultInternals; +} +export const ZodMiniPrefault: core.$constructor = /*@__PURE__*/ core.$constructor( + "ZodMiniPrefault", + (inst, def) => { + core.$ZodPrefault.init(inst, def); + ZodMiniType.init(inst, def); + } +); +export function prefault( + innerType: T, + defaultValue: util.NoUndefined> | (() => util.NoUndefined>) +): ZodMiniPrefault { + return new ZodMiniPrefault({ + type: "prefault", + innerType, + get defaultValue() { + return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue; + }, + }) as ZodMiniPrefault; } // ZodMiniNonOptional diff --git a/play.ts b/play.ts index 82831bb275..e69de29bb2 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);