diff --git a/packages/docs/components/ecosystem.tsx b/packages/docs/components/ecosystem.tsx index e963af0567..4c07febee4 100644 --- a/packages/docs/components/ecosystem.tsx +++ b/packages/docs/components/ecosystem.tsx @@ -38,7 +38,6 @@ const apiLibraries: ZodResource[] = [ // description: "Zod validator middleware for Hono", // slug: "honojs/middleware", // }, - ]; const formIntegrations: ZodResource[] = [ diff --git a/packages/zod/src/v3/helpers/partialUtil.ts b/packages/zod/src/v3/helpers/partialUtil.ts index b9be2e9b88..0eff8ff34c 100644 --- a/packages/zod/src/v3/helpers/partialUtil.ts +++ b/packages/zod/src/v3/helpers/partialUtil.ts @@ -10,31 +10,6 @@ import type { } from "../types.js"; export namespace partialUtil { - // export type DeepPartial = T extends AnyZodObject - // ? ZodObject< - // { [k in keyof T["_shape"]]: InternalDeepPartial }, - // T["_unknownKeys"], - // T["_catchall"] - // > - // : T extends ZodArray - // ? ZodArray, Card> - // : ZodOptional; - - // { - // // optional: T extends ZodOptional ? T : ZodOptional; - // // array: T extends ZodArray ? ZodArray> : never; - // object: T extends AnyZodObject - // ? ZodObject< - // { [k in keyof T["_shape"]]: DeepPartial }, - // T["_unknownKeys"], - // T["_catchall"] - // > - // : never; - // rest: ReturnType; // ZodOptional; - // }[T extends AnyZodObject - // ? "object" // T extends ZodOptional // ? 'optional' // : - // : "rest"]; - export type DeepPartial = T extends ZodObject ? ZodObject< { [k in keyof T["shape"]]: ZodOptional> }, @@ -56,20 +31,4 @@ export namespace partialUtil { : never : never : T; - // { - // // optional: T extends ZodOptional ? T : ZodOptional; - // // array: T extends ZodArray ? ZodArray> : never; - // object: T extends ZodObject - // ? ZodOptional< - // ZodObject< - // { [k in keyof Shape]: DeepPartial }, - // Params, - // Catchall - // > - // > - // : never; - // rest: ReturnType; - // }[T extends ZodObject - // ? "object" // T extends ZodOptional // ? 'optional' // : - // : "rest"]; } diff --git a/packages/zod/src/v4/classic/tests/lazy.test.ts b/packages/zod/src/v4/classic/tests/lazy.test.ts index f4db9d8cab..fb02348031 100644 --- a/packages/zod/src/v4/classic/tests/lazy.test.ts +++ b/packages/zod/src/v4/classic/tests/lazy.test.ts @@ -36,9 +36,14 @@ test("opt passthrough", () => { c: "default", }); - expect(z.lazy(() => z.string())._zod.optionality).toEqual(undefined); - expect(z.lazy(() => z.string().optional())._zod.optionality).toEqual("optional"); - expect(z.lazy(() => z.string().default("asdf"))._zod.optionality).toEqual("defaulted"); + expect(z.lazy(() => z.string())._zod.optin).toEqual(undefined); + expect(z.lazy(() => z.string())._zod.optout).toEqual(undefined); + + expect(z.lazy(() => z.string().optional())._zod.optin).toEqual("optional"); + expect(z.lazy(() => z.string().optional())._zod.optout).toEqual("optional"); + + expect(z.lazy(() => z.string().default("asdf"))._zod.optin).toEqual("optional"); + expect(z.lazy(() => z.string().default("asdf"))._zod.optout).toEqual(undefined); }); ////////////// LAZY ////////////// diff --git a/packages/zod/src/v4/classic/tests/optional.test.ts b/packages/zod/src/v4/classic/tests/optional.test.ts index 0de0083edb..1fc142b71a 100644 --- a/packages/zod/src/v4/classic/tests/optional.test.ts +++ b/packages/zod/src/v4/classic/tests/optional.test.ts @@ -16,3 +16,88 @@ test("unwrap", () => { const unwrapped = z.string().optional().unwrap(); expect(unwrapped).toBeInstanceOf(z.ZodString); }); + +test("optionality", () => { + const a = z.string(); + expect(a._zod.optin).toEqual(undefined); + expect(a._zod.optout).toEqual(undefined); + + const b = z.string().optional(); + expect(b._zod.optin).toEqual("optional"); + expect(b._zod.optout).toEqual("optional"); + + const c = z.string().default("asdf"); + expect(c._zod.optin).toEqual("optional"); + expect(c._zod.optout).toEqual(undefined); + + const d = z.string().optional().nullable(); + expect(d._zod.optin).toEqual("optional"); + expect(d._zod.optout).toEqual("optional"); + + const e = z.string().default("asdf").nullable(); + expect(e._zod.optin).toEqual("optional"); + expect(e._zod.optout).toEqual(undefined); +}); + +test("pipe optionality", () => { + z.string().optional()._zod.optin; + const a = z.string().optional().pipe(z.string()); + expect(a._zod.optin).toEqual("optional"); + expect(a._zod.optout).toEqual(undefined); + expectTypeOf().toEqualTypeOf<"optional">(); + expectTypeOf().toEqualTypeOf<"optional" | undefined>(); + + const b = z + .string() + .transform((val) => (Math.random() ? val : undefined)) + .pipe(z.string().optional()); + expect(b._zod.optin).toEqual(undefined); + expect(b._zod.optout).toEqual("optional"); + expectTypeOf().toEqualTypeOf<"optional" | undefined>(); + expectTypeOf().toEqualTypeOf<"optional">(); + + const c = z.string().default("asdf").pipe(z.string()); + expect(c._zod.optin).toEqual("optional"); + expect(c._zod.optout).toEqual(undefined); + + const d = z + .string() + .transform((val) => (Math.random() ? val : undefined)) + .pipe(z.string().default("asdf")); + expect(d._zod.optin).toEqual(undefined); + expect(d._zod.optout).toEqual(undefined); +}); + +test("pipe optionality inside objects", () => { + const schema = z.object({ + a: z.string().optional(), + b: z.string().optional().pipe(z.string()), + c: z.string().default("asdf").pipe(z.string()), + d: z + .string() + .transform((val) => (Math.random() ? val : undefined)) + .pipe(z.string().optional()), + e: z + .string() + .transform((val) => (Math.random() ? val : undefined)) + .pipe(z.string().default("asdf")), + }); + + type SchemaIn = z.input; + expectTypeOf().toEqualTypeOf<{ + a?: string | undefined; + b?: string | undefined; + c?: string | undefined; + d: string; + e: string; + }>(); + + type SchemaOut = z.output; + expectTypeOf().toEqualTypeOf<{ + a?: string | undefined; + b: string; + c: string; + d?: string | undefined; + e: string; + }>(); +}); diff --git a/packages/zod/src/v4/classic/tests/pickomit.test.ts b/packages/zod/src/v4/classic/tests/pickomit.test.ts index 5e8819c82d..39f45e8402 100644 --- a/packages/zod/src/v4/classic/tests/pickomit.test.ts +++ b/packages/zod/src/v4/classic/tests/pickomit.test.ts @@ -44,10 +44,10 @@ test("pick parse - fail", () => { test("pick - remove optional", () => { const schema = z.object({ a: z.string(), b: z.string().optional() }); - expect(schema._zod.def.shape.a._zod.optionality).toEqual(undefined); - expect(schema._zod.def.shape.b!._zod.optionality).toEqual("optional"); + expect("a" in schema._zod.def.shape).toEqual(true); + expect("b" in schema._zod.def.shape!).toEqual(true); const picked = schema.pick({ a: true }); - expect(picked._zod.def.shape.a._zod.optionality).toEqual(undefined); + expect("a" in picked._zod.def.shape).toEqual(true); expect("b" in picked._zod.def.shape!).toEqual(false); }); @@ -85,9 +85,9 @@ test("omit parse - fail", () => { test("omit - remove optional", () => { const schema = z.object({ a: z.string(), b: z.string().optional() }); + expect("a" in schema._zod.def.shape).toEqual(true); const omitted = schema.omit({ a: true }); expect("a" in omitted._zod.def.shape).toEqual(false); - expect(omitted._zod.def.shape.b!._zod.optionality).toEqual("optional"); }); test("nonstrict inference", () => { diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index 8f0eda261a..9afacb5b1f 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -114,7 +114,9 @@ export interface $ZodTypeInternals { /** @internal Indicates that a schema output type should be considered optional inside objects. * @default Required */ - optionality?: "optional" | "defaulted" | undefined; + + optin?: "optional" | undefined; + optout?: "optional" | undefined; /** @internal A set of literal discriminators used for the fast path in discriminated unions. */ disc: util.DiscriminatorMap | undefined; @@ -1597,8 +1599,8 @@ export interface $ZodObjectInternals< } export type $ZodLooseShape = Record; -type OptionalOutSchema = { _zod: { optionality: "optional" } }; -type OptionalInSchema = { _zod: { optionality: "defaulted" | "optional" } }; +type OptionalOutSchema = { _zod: { optout: "optional" } }; +type OptionalInSchema = { _zod: { optin: "optional" } }; export type $InferObjectOutput> = string extends keyof T ? object @@ -1634,6 +1636,7 @@ function handleObjectResult(result: ParsePayload, final: ParsePayload, key: Prop } function handleOptionalObjectResult(result: ParsePayload, final: ParsePayload, key: PropertyKey, input: any) { + // console.dir({ key, result }, { depth: null }); if (result.issues.length) { // validation failed against value schema if (input[key] === undefined) { @@ -1687,6 +1690,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con const _normalized = util.cached(() => { const keys = Object.keys(def.shape); const okeys = util.optionalKeys(def.shape); + console.dir(okeys, { depth: null }); return { shape: def.shape, keys, @@ -1720,6 +1724,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con const generateFastpass = (shape: any) => { const doc = new Doc(["shape", "payload", "ctx"]); const { keys, optionalKeys } = _normalized.value; + console.dir({ keys, optionalKeys }, { depth: null }); const parseStr = (key: string) => { const k = util.esc(key); @@ -1740,6 +1745,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con const id = ids[key]; doc.write(`const ${id} = ${parseStr(key)};`); const k = util.esc(key); + doc.write(`console.log(${id});`); doc.write(` if (${id}.issues.length) { if (input[${k}] === undefined) { @@ -1757,7 +1763,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con } else if (${id}.value === undefined) { if (${k} in input) newResult[${k}] = undefined; } else { - if (${k} in input) newResult[${k}] = ${id}.value; + newResult[${k}] = ${id}.value; } `); } else { @@ -1830,7 +1836,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con // } const r = el._zod.run({ value: input[key], issues: [] }, ctx); - const isOptional = el._zod.optionality === "optional"; + const isOptional = el._zod.optin === "optional"; if (r instanceof Promise) { proms.push( @@ -2258,7 +2264,7 @@ type TupleInputTypeWithOptionals = T extends readonly ...infer Prefix extends $ZodType[], infer Tail extends $ZodType, ] - ? Tail["_zod"]["optionality"] extends "optional" | "defaulted" + ? Tail["_zod"]["optin"] extends "optional" ? [...TupleInputTypeWithOptionals, Tail["_zod"]["input"]?] : TupleInputTypeNoOptionals : []; @@ -2274,7 +2280,7 @@ type TupleOutputTypeWithOptionals = T extends readonl ...infer Prefix extends $ZodType[], infer Tail extends $ZodType, ] - ? Tail["_zod"]["optionality"] extends "optional" + ? Tail["_zod"]["optout"] extends "optional" ? [...TupleOutputTypeWithOptionals, core.output?] : TupleOutputTypeNoOptionals : []; @@ -2295,7 +2301,7 @@ export interface $ZodTuple = /*@__PURE__*/ core.$constructor("$ZodTuple", (inst, def) => { $ZodType.init(inst, def); const items = def.items; - const optStart = items.length - [...items].reverse().findIndex((item) => item._zod.optionality !== "optional"); + const optStart = items.length - [...items].reverse().findIndex((item) => item._zod.optin !== "optional"); inst._zod.parse = (payload, ctx) => { const input = payload.value; @@ -2953,7 +2959,8 @@ export interface $ZodOptionalDef extends $ZodType export interface $ZodOptionalInternals extends $ZodTypeInternals | undefined, core.input | undefined> { def: $ZodOptionalDef; - optionality: "optional"; + optin: "optional"; + optout: "optional"; isst: never; values: T["_zod"]["values"]; pattern: T["_zod"]["pattern"]; @@ -2967,7 +2974,8 @@ export const $ZodOptional: core.$constructor<$ZodOptional> = /*@__PURE__*/ core. "$ZodOptional", (inst, def) => { $ZodType.init(inst, def); - inst._zod.optionality = "optional"; + inst._zod.optin = "optional"; + inst._zod.optout = "optional"; util.defineLazy(inst._zod, "values", () => { console.dir("VALUES", { depth: null }); @@ -3002,7 +3010,8 @@ export interface $ZodNullableDef extends $ZodType export interface $ZodNullableInternals extends $ZodTypeInternals | null, core.input | null> { def: $ZodNullableDef; - optionality: T["_zod"]["optionality"]; + optin: T["_zod"]["optin"]; + optout: T["_zod"]["optout"]; isst: never; values: T["_zod"]["values"]; pattern: T["_zod"]["pattern"]; @@ -3016,7 +3025,8 @@ export const $ZodNullable: core.$constructor<$ZodNullable> = /*@__PURE__*/ core. "$ZodNullable", (inst, def) => { $ZodType.init(inst, def); - util.defineLazy(inst._zod, "optionality", () => def.innerType._zod.optionality); + util.defineLazy(inst._zod, "optin", () => def.innerType._zod.optin); + util.defineLazy(inst._zod, "optout", () => def.innerType._zod.optout); util.defineLazy(inst._zod, "pattern", () => { const pattern = def.innerType._zod.pattern; @@ -3052,8 +3062,7 @@ export interface $ZodDefaultDef extends $ZodTypeD export interface $ZodDefaultInternals extends $ZodTypeInternals>, core.input | undefined> { def: $ZodDefaultDef; - // qin: "true"; - optionality: "defaulted"; + optin: "optional"; isst: never; values: T["_zod"]["values"]; } @@ -3068,7 +3077,7 @@ export const $ZodDefault: core.$constructor<$ZodDefault> = /*@__PURE__*/ core.$c $ZodType.init(inst, def); // inst._zod.qin = "true"; - inst._zod.optionality = "defaulted"; + inst._zod.optin = "optional"; util.defineLazy(inst._zod, "values", () => def.innerType._zod.values); inst._zod.parse = (payload, ctx) => { @@ -3113,7 +3122,7 @@ export interface $ZodPrefaultDef extends $ZodType export interface $ZodPrefaultInternals extends $ZodTypeInternals>, core.input | undefined> { def: $ZodPrefaultDef; - optionality: "defaulted"; + optin: "optional"; isst: never; values: T["_zod"]["values"]; } @@ -3127,7 +3136,7 @@ export const $ZodPrefault: core.$constructor<$ZodPrefault> = /*@__PURE__*/ core. (inst, def) => { $ZodType.init(inst, def); - inst._zod.optionality = "defaulted"; + inst._zod.optin = "optional"; util.defineLazy(inst._zod, "values", () => def.innerType._zod.values); inst._zod.parse = (payload, ctx) => { @@ -3299,7 +3308,8 @@ export interface $ZodCatchInternals // qin: T["_zod"]["qin"]; // qout: T["_zod"]["qout"]; - optionality: T["_zod"]["optionality"]; + optin: T["_zod"]["optin"]; + optout: T["_zod"]["optout"]; isst: never; values: T["_zod"]["values"]; } @@ -3310,7 +3320,8 @@ export interface $ZodCatch extends $ZodType { export const $ZodCatch: core.$constructor<$ZodCatch> = /*@__PURE__*/ core.$constructor("$ZodCatch", (inst, def) => { $ZodType.init(inst, def); - util.defineLazy(inst._zod, "optionality", () => def.innerType._zod.optionality); + util.defineLazy(inst._zod, "optin", () => def.innerType._zod.optin); + util.defineLazy(inst._zod, "optout", () => def.innerType._zod.optout); util.defineLazy(inst._zod, "values", () => def.innerType._zod.values); inst._zod.parse = (payload, ctx) => { @@ -3404,6 +3415,8 @@ export interface $ZodPipeInternals; isst: never; values: A["_zod"]["values"]; + optin: A["_zod"]["optin"]; + optout: B["_zod"]["optout"]; } export interface $ZodPipe extends $ZodType { @@ -3412,9 +3425,9 @@ export interface $ZodPipe = /*@__PURE__*/ core.$constructor("$ZodPipe", (inst, def) => { $ZodType.init(inst, def); - // inst._zod.qin = def.in._zod.qin; - // inst._zod.qout = def.in._zod.qout; - inst._zod.values = def.in._zod.values; + util.defineLazy(inst._zod, "values", () => def.in._zod.values); + util.defineLazy(inst._zod, "optin", () => def.in._zod.optin); + util.defineLazy(inst._zod, "optout", () => def.out._zod.optout); inst._zod.parse = (payload, ctx) => { const left = def.in._zod.run(payload, ctx); @@ -3449,9 +3462,8 @@ export interface $ZodReadonlyDef extends $ZodTypeDef { export interface $ZodReadonlyInternals extends $ZodTypeInternals>, util.MakeReadonly>> { def: $ZodReadonlyDef; - // qin: T["_zod"]["qin"]; - // qout: T["_zod"]["qout"]; - optionality: T["_zod"]["optionality"]; + optin: T["_zod"]["optin"]; + optout: T["_zod"]["optout"]; isst: never; disc: T["_zod"]["disc"]; } @@ -3465,7 +3477,8 @@ export const $ZodReadonly: core.$constructor<$ZodReadonly> = /*@__PURE__*/ core. (inst, def) => { $ZodType.init(inst, def); util.defineLazy(inst._zod, "disc", () => def.innerType._zod.disc); - util.defineLazy(inst._zod, "optionality", () => def.innerType._zod.optionality); + util.defineLazy(inst._zod, "optin", () => def.innerType._zod.optin); + util.defineLazy(inst._zod, "optout", () => def.innerType._zod.optout); inst._zod.parse = (payload, ctx) => { const result = def.innerType._zod.run(payload, ctx); @@ -3638,9 +3651,8 @@ export interface $ZodLazyInternals innerType: T; pattern: T["_zod"]["pattern"]; disc: T["_zod"]["disc"]; - // qin: T["_zod"]["qin"]; - // qout: T["_zod"]["qout"]; - optionality: T["_zod"]["optionality"]; + optin: T["_zod"]["optin"]; + optout: T["_zod"]["optout"]; } export interface $ZodLazy extends $ZodType { @@ -3653,7 +3665,8 @@ export const $ZodLazy: core.$constructor<$ZodLazy> = /*@__PURE__*/ core.$constru util.defineLazy(inst._zod, "innerType", () => def.getter()); util.defineLazy(inst._zod, "pattern", () => inst._zod.innerType._zod.pattern); util.defineLazy(inst._zod, "disc", () => inst._zod.innerType._zod.disc); - util.defineLazy(inst._zod, "optionality", () => inst._zod.innerType._zod.optionality); + util.defineLazy(inst._zod, "optin", () => inst._zod.innerType._zod.optin); + util.defineLazy(inst._zod, "optout", () => inst._zod.innerType._zod.optout); inst._zod.parse = (payload, ctx) => { const inner = inst._zod.innerType; return inner._zod.run(payload, ctx); diff --git a/packages/zod/src/v4/core/to-json-schema.ts b/packages/zod/src/v4/core/to-json-schema.ts index 8d9effaf46..ebc756085a 100644 --- a/packages/zod/src/v4/core/to-json-schema.ts +++ b/packages/zod/src/v4/core/to-json-schema.ts @@ -273,11 +273,11 @@ export class JSONSchemaGenerator { // const optionalKeys = new Set(def.optional); const requiredKeys = new Set( [...allKeys].filter((key) => { - const opt = def.shape[key]._zod.optionality; + const v = def.shape[key]._zod; if (this.io === "input") { - return opt === undefined; + return v.optin === undefined; } else { - return opt === undefined || opt === "defaulted"; + return v.optout === undefined; } }) ); diff --git a/packages/zod/src/v4/core/util.ts b/packages/zod/src/v4/core/util.ts index d235fdb126..0537bfc43e 100644 --- a/packages/zod/src/v4/core/util.ts +++ b/packages/zod/src/v4/core/util.ts @@ -489,7 +489,7 @@ export function stringifyPrimitive(value: any): string { export function optionalKeys(shape: schemas.$ZodShape): string[] { return Object.keys(shape).filter((k) => { - return shape[k]._zod.optionality === "optional"; + return shape[k]._zod.optin === "optional"; }); } diff --git a/packages/zod/src/v4/mini/tests/object.test.ts b/packages/zod/src/v4/mini/tests/object.test.ts index 46fad84983..24c422e657 100644 --- a/packages/zod/src/v4/mini/tests/object.test.ts +++ b/packages/zod/src/v4/mini/tests/object.test.ts @@ -10,7 +10,7 @@ test("z.object", () => { }); a._zod.def.shape["test?"]; - a._zod.def.shape.points._zod.optionality; + a._zod.def.shape.points._zod.optin; type a = z.output; diff --git a/play.ts b/play.ts index 22dbc4e97e..f028c0870c 100644 --- a/play.ts +++ b/play.ts @@ -1,24 +1,10 @@ import * as z from "zod/v4"; -// console.dir(z.literal("asdf").optional()._zod.values, { depth: null }); - -const BaseError = z.object({ status: z.literal("failed"), message: z.string() }); -const MyErrors = z.discriminatedUnion("code", [ - BaseError.extend({ code: z.literal(400) }), - BaseError.extend({ code: z.literal(401) }), - BaseError.extend({ code: z.literal(500) }), -]); - -const MyResult = z.discriminatedUnion("status", [ - z.object({ status: z.literal("success"), data: z.string() }), - MyErrors, -]); - -const result = MyResult.parse({ status: "success", data: "hello" }); -console.log(result); // { status: 'success', data: 'hello' } -const result2 = MyResult.parse({ status: "failed", code: 400, message: "bad request" }); -console.log(result2); // { status: 'failed', code: 400, message: 'bad request' } -const result3 = MyResult.parse({ status: "failed", code: 401, message: "unauthorized" }); -console.log(result3); // { status: 'failed', code: 401, message: 'unauthorized' } -const result4 = MyResult.parse({ status: "failed", code: 500, message: "internal server error" }); -console.log(result4); // { status: 'failed', code: 500, message: 'internal server error' } +const data = z + .object({ + foo: z.boolean().nullable().default(true), + bar: z.boolean().default(true), + }) + .parse({ foo: null }, { jitless: false }); + +console.dir(data, { depth: null });