diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts index b2187d22e1..a1a0d001cd 100644 --- a/packages/zod/src/v4/classic/schemas.ts +++ b/packages/zod/src/v4/classic/schemas.ts @@ -1055,37 +1055,33 @@ export function keyof(schema: T): ZodLiteral = Record, - InExtra extends Record = Record, + out Config extends core.$ZodObjectConfig = core.$ZodObjectConfig, > extends ZodType { - _zod: core.$ZodObjectInternals; + _zod: core.$ZodObjectInternals; shape: Shape; keyof(): ZodEnum>; /** Define a schema to validate all unrecognized keys. This overrides the existing strict/loose behavior. */ - catchall(schema: T): ZodObject>; + catchall(schema: T): ZodObject>; /** @deprecated Use `z.looseObject()` or `.loose()` instead. */ - passthrough(): ZodObject>; + passthrough(): ZodObject; /** Consider `z.looseObject(A.shape)` instead */ - loose(): ZodObject>; + loose(): ZodObject; /** Consider `z.strictObject(A.shape)` instead */ - strict(): ZodObject; + strict(): ZodObject; /** This is the default behavior. This method call is likely unnecessary. */ - strip(): ZodObject; + strip(): ZodObject; extend>>( shape: U - ): ZodObject< - util.Extend, - OutExtra, - InExtra // & B['_zod']["extra"] - >; + ): ZodObject, Config>; /** * @deprecated Use destructuring to merge the shapes: @@ -1097,24 +1093,21 @@ export interface ZodObject< * }); * ``` */ - merge( - other: U - ): ZodObject, U["_zod"]["outextra"], U["_zod"]["inextra"]>; + merge(other: U): ZodObject, U["_zod"]["config"]>; pick, M>>( mask: M - ): ZodObject>>, OutExtra, InExtra>; + ): ZodObject>>, Config>; omit, M>>( mask: M - ): ZodObject>>, OutExtra, InExtra>; + ): ZodObject>>, Config>; partial(): ZodObject< { [k in keyof Shape]: ZodOptional; }, - OutExtra, - InExtra + Config >; partial, M>>( mask: M @@ -1122,8 +1115,7 @@ export interface ZodObject< { [k in keyof Shape]: k extends keyof M ? ZodOptional : Shape[k]; }, - OutExtra, - InExtra + Config >; // required @@ -1131,8 +1123,7 @@ export interface ZodObject< { [k in keyof Shape]: ZodNonOptional; }, - OutExtra, - InExtra + Config >; required, M>>( mask: M @@ -1140,8 +1131,7 @@ export interface ZodObject< { [k in keyof Shape]: k extends keyof M ? ZodNonOptional : Shape[k]; }, - OutExtra, - InExtra + Config >; } @@ -1173,7 +1163,7 @@ export const ZodObject: core.$constructor = /*@__PURE__*/ core.$const export function object>>( shape?: T, params?: string | core.$ZodObjectParams -): ZodObject & {}, {}, {}> { +): ZodObject & {}, core.$strip> { const def: core.$ZodObjectDef = { type: "object", @@ -1192,7 +1182,7 @@ export function object( shape: T, params?: string | core.$ZodObjectParams -): ZodObject { +): ZodObject { return new ZodObject({ type: "object", @@ -1207,10 +1197,11 @@ export function strictObject( } // looseObject + export function looseObject( shape: T, params?: string | core.$ZodObjectParams -): ZodObject { +): ZodObject { return new ZodObject({ type: "object", diff --git a/packages/zod/src/v4/classic/tests/intersection.test.ts b/packages/zod/src/v4/classic/tests/intersection.test.ts index 76d045f10a..d1538c03c3 100644 --- a/packages/zod/src/v4/classic/tests/intersection.test.ts +++ b/packages/zod/src/v4/classic/tests/intersection.test.ts @@ -21,7 +21,7 @@ test("object intersection: loose", () => { const C = z.intersection(A, B); // BaseC.merge(HasID); type C = z.infer; - expectTypeOf().toEqualTypeOf<{ a: string } & { b: string } & Record>(); + expectTypeOf().toEqualTypeOf<{ a: string; [x: string]: unknown } & { b: string }>(); const data = { a: "foo", b: "foo", c: "extra" }; expect(C.parse(data)).toEqual(data); expect(() => C.parse({ a: "foo" })).toThrow(); diff --git a/packages/zod/src/v4/classic/tests/object.test.ts b/packages/zod/src/v4/classic/tests/object.test.ts index 3c58c6b601..0429eade50 100644 --- a/packages/zod/src/v4/classic/tests/object.test.ts +++ b/packages/zod/src/v4/classic/tests/object.test.ts @@ -417,7 +417,7 @@ test("passthrough index signature", () => { expectTypeOf().toEqualTypeOf<{ a: string }>(); const b = a.passthrough(); type b = z.infer; - expectTypeOf().toEqualTypeOf<{ a: string } & { [k: string]: unknown }>(); + expectTypeOf().toEqualTypeOf<{ a: string; [k: string]: unknown }>(); }); // test("xor", () => { @@ -454,3 +454,23 @@ test("null prototype", () => { obj.a = "foo"; expect(schema.parse(obj)).toEqual({ a: "foo" }); }); + +test("empty objects", () => { + const A = z.looseObject({}); + type Ain = z.input; + expectTypeOf().toEqualTypeOf>(); + type Aout = z.output; + expectTypeOf().toEqualTypeOf>(); + + const B = z.object({}); + type Bout = z.output; + expectTypeOf().toEqualTypeOf>(); + type Bin = z.input; + expectTypeOf().toEqualTypeOf>(); + + const C = z.strictObject({}); + type Cout = z.output; + expectTypeOf().toEqualTypeOf>(); + type Cin = z.input; + expectTypeOf().toEqualTypeOf>(); +}); diff --git a/packages/zod/src/v4/classic/tests/pickomit-object.test.ts b/packages/zod/src/v4/classic/tests/pickomit.test.ts similarity index 98% rename from packages/zod/src/v4/classic/tests/pickomit-object.test.ts rename to packages/zod/src/v4/classic/tests/pickomit.test.ts index 30a0d83bd5..5e8819c82d 100644 --- a/packages/zod/src/v4/classic/tests/pickomit-object.test.ts +++ b/packages/zod/src/v4/classic/tests/pickomit.test.ts @@ -93,7 +93,7 @@ test("omit - remove optional", () => { test("nonstrict inference", () => { const laxfish = fish.pick({ name: true }).catchall(z.any()); type laxfish = z.infer; - expectTypeOf().toEqualTypeOf<{ name: string } & { [k: string]: any }>(); + expectTypeOf().toEqualTypeOf<{ name: string; [k: string]: any }>(); }); test("nonstrict parsing - pass", () => { diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index a7c1bfc9ca..c0086c0c7d 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -1565,20 +1565,18 @@ export interface $ZodObjectDef extends $Zod export interface $ZodObjectInternals< // @ts-ignore Cast variance out Shape extends Readonly<$ZodShape> = Readonly<$ZodShape>, - out OutExtra extends Record = Record, - out InExtra extends Record = Record, + out Config extends $ZodObjectConfig = $ZodObjectConfig, > extends $ZodTypeInternals { def: $ZodObjectDef; - outextra: OutExtra; - inextra: InExtra; + config: Config; isst: errors.$ZodIssueInvalidType | errors.$ZodIssueUnrecognizedKeys; disc: util.DiscriminatorMap; // special keys only used for objects // not defined on $ZodTypeInternals (base interface) because it breaks cyclical inference // the z.infer<> util checks for these first when extracting inferred type - "~output": $InferObjectOutput; - "~input": $InferObjectInput; + "~output": $InferObjectOutput; + "~input": $InferObjectInput; } export type $ZodLooseShape = Record; @@ -1587,26 +1585,25 @@ type OptionalInSchema = { _zod: { optionality: "defaulted" | "optional" } }; export type $InferObjectOutput> = string extends keyof T ? object - : keyof T extends never + : keyof (T & Extra) extends never ? Record : util.Prettify< { -readonly [k in keyof T as T[k] extends OptionalOutSchema ? never : k]: core.output; } & { -readonly [k in keyof T as T[k] extends OptionalOutSchema ? k : never]?: core.output; - } - > & - Extra; + } & Extra + >; export type $InferObjectInput> = string extends keyof T ? object - : keyof T extends never + : keyof (T & Extra) extends never ? Record : util.Prettify< { - [k in keyof T as T[k] extends OptionalInSchema ? never : k]: core.input; + -readonly [k in keyof T as T[k] extends OptionalInSchema ? never : k]: core.input; } & { - [k in keyof T as T[k] extends OptionalInSchema ? k : never]?: core.input; + -readonly [k in keyof T as T[k] extends OptionalInSchema ? k : never]?: core.input; } & Extra >; @@ -1641,13 +1638,32 @@ function handleOptionalObjectResult(result: ParsePayload, final: ParsePayload, k } } +export type $ZodObjectConfig = { out: Record; in: Record }; + +export type $loose = { + out: Record; + in: Record; +}; +export type $strict = { + out: {}; + in: {}; +}; +export type $strip = { + out: {}; + in: {}; +}; +export type $catchall = { + out: { [k: string]: core.output }; + in: { [k: string]: core.input }; +}; export interface $ZodObject< // @ts-ignore Cast variance out Shape extends Readonly<$ZodShape> = Readonly<$ZodShape>, - out OutExtra extends Record = Record, - out InExtra extends Record = Record, + out Params extends $ZodObjectConfig = $ZodObjectConfig, + // out OutExtra extends Record = Record, + // out InExtra extends Record = Record, > extends $ZodType { - _zod: $ZodObjectInternals; + _zod: $ZodObjectInternals; } export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$constructor("$ZodObject", (inst, def) => { diff --git a/packages/zod/src/v4/mini/schemas.ts b/packages/zod/src/v4/mini/schemas.ts index 840f72eaf8..ef86000d22 100644 --- a/packages/zod/src/v4/mini/schemas.ts +++ b/packages/zod/src/v4/mini/schemas.ts @@ -694,10 +694,9 @@ export function keyof(schema: T): ZodMiniLiteral = Record, - InExtra extends Record = Record, + out Config extends core.$ZodObjectConfig = core.$ZodObjectConfig, > extends ZodMiniType { - _zod: core.$ZodObjectInternals; + _zod: core.$ZodObjectInternals; shape: Shape; } export const ZodMiniObject: core.$constructor = /*@__PURE__*/ core.$constructor( @@ -710,7 +709,7 @@ export const ZodMiniObject: core.$constructor = /*@__PURE__*/ cor export function object>( shape?: T, params?: string | core.$ZodObjectParams -): ZodMiniObject { +): ZodMiniObject { const def: core.$ZodObjectDef = { type: "object", get shape() { @@ -727,7 +726,7 @@ export function object>( export function strictObject( shape: T, params?: string | core.$ZodObjectParams -): ZodMiniObject { +): ZodMiniObject { return new ZodMiniObject({ type: "object", // shape: shape as core.$ZodLooseShape, @@ -748,7 +747,7 @@ export function strictObject( export function looseObject( shape: T, params?: string | core.$ZodObjectParams -): ZodMiniObject { +): ZodMiniObject { return new ZodMiniObject({ type: "object", // shape: shape as core.$ZodLooseShape, @@ -768,7 +767,7 @@ export function looseObject( export function extend( schema: T, shape: U -): ZodMiniObject, T["_zod"]["outextra"], T["_zod"]["inextra"]> { +): ZodMiniObject, T["_zod"]["config"]> { return util.extend(schema, shape); } @@ -776,7 +775,7 @@ export function extend( export function merge( a: T, b: U -): ZodMiniObject, T["_zod"]["outextra"], T["_zod"]["inextra"]>; +): ZodMiniObject, T["_zod"]["config"]>; export function merge(schema: ZodMiniObject, shape: any): ZodMiniObject { return util.extend(schema, shape); } @@ -784,11 +783,7 @@ export function merge(schema: ZodMiniObject, shape: any): ZodMiniObject { export function pick>( schema: T, mask: M -): ZodMiniObject< - util.Flatten>, - T["_zod"]["outextra"], - T["_zod"]["inextra"] -> { +): ZodMiniObject>, T["_zod"]["config"]> { return util.pick(schema, mask as any); } @@ -797,7 +792,7 @@ export function pick>( schema: T, mask: M -): ZodMiniObject>, T["_zod"]["outextra"], T["_zod"]["inextra"]> { +): ZodMiniObject>, T["_zod"]["config"]> { return util.omit(schema, mask); } @@ -807,8 +802,7 @@ export function partial( { [k in keyof T["shape"]]: ZodMiniOptional; }, - T["_zod"]["outextra"], - T["_zod"]["inextra"] + T["_zod"]["config"] >; export function partial>( schema: T, @@ -817,8 +811,7 @@ export function partial : T["shape"][k]; }, - T["_zod"]["outextra"], - T["_zod"]["inextra"] + T["_zod"]["config"] >; export function partial(schema: ZodMiniObject, mask?: object) { return util.partial(ZodMiniOptional, schema, mask); @@ -841,8 +834,7 @@ export function required( { [k in keyof T["shape"]]: ZodMiniNonOptional; }, - T["_zod"]["outextra"], - T["_zod"]["inextra"] + T["_zod"]["config"] >; export function required>( schema: T, @@ -854,8 +846,7 @@ export function required; } >, - T["_zod"]["outextra"], - T["_zod"]["inextra"] + T["_zod"]["config"] >; export function required(schema: ZodMiniObject, mask?: object) { return util.required(ZodMiniNonOptional, schema, mask); diff --git a/play.ts b/play.ts index 0c4bfce9c0..1263049de4 100644 --- a/play.ts +++ b/play.ts @@ -2,17 +2,11 @@ import { z } from "zod/v4"; z; -const schema = z.object({ - name: z.string(), - logLevel: z.union([z.string(), z.number()]), - env: z.literal(["production", "development"]), -}); - -const data = { - name: 1000, - logLevel: false, -}; - -const result = schema.safeParse(data); - -console.dir(z.prettifyError(result.error!), { depth: null }); +const stringWithDefault = z + .pipe( + z.transform((v) => (v === "none" ? undefined : v)), + z.string() + ) + .catch("default"); + +stringWithDefault.parse(undefined);