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
49 changes: 20 additions & 29 deletions packages/zod/src/v4/classic/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,37 +1055,33 @@ export function keyof<T extends ZodObject>(schema: T): ZodLiteral<keyof T["_zod"
}

// ZodObject

export interface ZodObject<
// @ts-ignore cast variance
out Shape extends core.$ZodShape = core.$ZodLooseShape,
OutExtra extends Record<string, unknown> = Record<string, unknown>,
InExtra extends Record<string, unknown> = Record<string, unknown>,
out Config extends core.$ZodObjectConfig = core.$ZodObjectConfig,
> extends ZodType {
_zod: core.$ZodObjectInternals<Shape, OutExtra, InExtra>;
_zod: core.$ZodObjectInternals<Shape, Config>;
shape: Shape;

keyof(): ZodEnum<util.ToEnum<keyof Shape & string>>;
/** Define a schema to validate all unrecognized keys. This overrides the existing strict/loose behavior. */
catchall<T extends core.$ZodType>(schema: T): ZodObject<Shape, Record<string, T["_zod"]["output"]>>;
catchall<T extends core.$ZodType>(schema: T): ZodObject<Shape, core.$catchall<T>>;

/** @deprecated Use `z.looseObject()` or `.loose()` instead. */
passthrough(): ZodObject<Shape, Record<string, unknown>>;
passthrough(): ZodObject<Shape, core.$loose>;
/** Consider `z.looseObject(A.shape)` instead */
loose(): ZodObject<Shape, Record<string, unknown>>;
loose(): ZodObject<Shape, core.$loose>;

/** Consider `z.strictObject(A.shape)` instead */
strict(): ZodObject<Shape, {}>;
strict(): ZodObject<Shape, core.$strict>;

/** This is the default behavior. This method call is likely unnecessary. */
strip(): ZodObject<Shape, {}>;
strip(): ZodObject<Shape, core.$strict>;

extend<U extends core.$ZodLooseShape & Partial<Record<keyof Shape, core.$ZodType>>>(
shape: U
): ZodObject<
util.Extend<Shape, U>,
OutExtra,
InExtra // & B['_zod']["extra"]
>;
): ZodObject<util.Extend<Shape, U>, Config>;

/**
* @deprecated Use destructuring to merge the shapes:
Expand All @@ -1097,51 +1093,45 @@ export interface ZodObject<
* });
* ```
*/
merge<U extends ZodObject>(
other: U
): ZodObject<util.Extend<Shape, U["shape"]>, U["_zod"]["outextra"], U["_zod"]["inextra"]>;
merge<U extends ZodObject>(other: U): ZodObject<util.Extend<Shape, U["shape"]>, U["_zod"]["config"]>;

pick<M extends util.Exactly<util.Mask<keyof Shape>, M>>(
mask: M
): ZodObject<util.Flatten<Pick<Shape, Extract<keyof Shape, keyof M>>>, OutExtra, InExtra>;
): ZodObject<util.Flatten<Pick<Shape, Extract<keyof Shape, keyof M>>>, Config>;

omit<M extends util.Exactly<util.Mask<keyof Shape>, M>>(
mask: M
): ZodObject<util.Flatten<Omit<Shape, Extract<keyof Shape, keyof M>>>, OutExtra, InExtra>;
): ZodObject<util.Flatten<Omit<Shape, Extract<keyof Shape, keyof M>>>, Config>;

partial(): ZodObject<
{
[k in keyof Shape]: ZodOptional<Shape[k]>;
},
OutExtra,
InExtra
Config
>;
partial<M extends util.Exactly<util.Mask<keyof Shape>, M>>(
mask: M
): ZodObject<
{
[k in keyof Shape]: k extends keyof M ? ZodOptional<Shape[k]> : Shape[k];
},
OutExtra,
InExtra
Config
>;

// required
required(): ZodObject<
{
[k in keyof Shape]: ZodNonOptional<Shape[k]>;
},
OutExtra,
InExtra
Config
>;
required<M extends util.Exactly<util.Mask<keyof Shape>, M>>(
mask: M
): ZodObject<
{
[k in keyof Shape]: k extends keyof M ? ZodNonOptional<Shape[k]> : Shape[k];
},
OutExtra,
InExtra
Config
>;
}

Expand Down Expand Up @@ -1173,7 +1163,7 @@ export const ZodObject: core.$constructor<ZodObject> = /*@__PURE__*/ core.$const
export function object<T extends core.$ZodLooseShape = Partial<Record<never, core.$ZodType>>>(
shape?: T,
params?: string | core.$ZodObjectParams
): ZodObject<util.Writeable<T> & {}, {}, {}> {
): ZodObject<util.Writeable<T> & {}, core.$strip> {
const def: core.$ZodObjectDef = {
type: "object",

Expand All @@ -1192,7 +1182,7 @@ export function object<T extends core.$ZodLooseShape = Partial<Record<never, cor
export function strictObject<T extends core.$ZodLooseShape>(
shape: T,
params?: string | core.$ZodObjectParams
): ZodObject<T, {}, {}> {
): ZodObject<T, core.$strict> {
return new ZodObject({
type: "object",

Expand All @@ -1207,10 +1197,11 @@ export function strictObject<T extends core.$ZodLooseShape>(
}

// looseObject

export function looseObject<T extends core.$ZodLooseShape>(
shape: T,
params?: string | core.$ZodObjectParams
): ZodObject<T, { [k: string]: unknown }, { [k: string]: unknown }> {
): ZodObject<T, core.$loose> {
return new ZodObject({
type: "object",

Expand Down
2 changes: 1 addition & 1 deletion packages/zod/src/v4/classic/tests/intersection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test("object intersection: loose", () => {

const C = z.intersection(A, B); // BaseC.merge(HasID);
type C = z.infer<typeof C>;
expectTypeOf<C>().toEqualTypeOf<{ a: string } & { b: string } & Record<string, unknown>>();
expectTypeOf<C>().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();
Expand Down
22 changes: 21 additions & 1 deletion packages/zod/src/v4/classic/tests/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ test("passthrough index signature", () => {
expectTypeOf<a>().toEqualTypeOf<{ a: string }>();
const b = a.passthrough();
type b = z.infer<typeof b>;
expectTypeOf<b>().toEqualTypeOf<{ a: string } & { [k: string]: unknown }>();
expectTypeOf<b>().toEqualTypeOf<{ a: string; [k: string]: unknown }>();
});

// test("xor", () => {
Expand Down Expand Up @@ -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<typeof A>;
expectTypeOf<Ain>().toEqualTypeOf<Record<string, unknown>>();
type Aout = z.output<typeof A>;
expectTypeOf<Aout>().toEqualTypeOf<Record<string, unknown>>();

const B = z.object({});
type Bout = z.output<typeof B>;
expectTypeOf<Bout>().toEqualTypeOf<Record<string, never>>();
type Bin = z.input<typeof B>;
expectTypeOf<Bin>().toEqualTypeOf<Record<string, never>>();

const C = z.strictObject({});
type Cout = z.output<typeof C>;
expectTypeOf<Cout>().toEqualTypeOf<Record<string, never>>();
type Cin = z.input<typeof C>;
expectTypeOf<Cin>().toEqualTypeOf<Record<string, never>>();
});
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ test("omit - remove optional", () => {
test("nonstrict inference", () => {
const laxfish = fish.pick({ name: true }).catchall(z.any());
type laxfish = z.infer<typeof laxfish>;
expectTypeOf<laxfish>().toEqualTypeOf<{ name: string } & { [k: string]: any }>();
expectTypeOf<laxfish>().toEqualTypeOf<{ name: string; [k: string]: any }>();
});

test("nonstrict parsing - pass", () => {
Expand Down
48 changes: 32 additions & 16 deletions packages/zod/src/v4/core/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1565,20 +1565,18 @@ export interface $ZodObjectDef<Shape extends $ZodShape = $ZodShape> extends $Zod
export interface $ZodObjectInternals<
// @ts-ignore Cast variance
out Shape extends Readonly<$ZodShape> = Readonly<$ZodShape>,
out OutExtra extends Record<string, unknown> = Record<string, unknown>,
out InExtra extends Record<string, unknown> = Record<string, unknown>,
out Config extends $ZodObjectConfig = $ZodObjectConfig,
> extends $ZodTypeInternals<any, any> {
def: $ZodObjectDef<Shape>;
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<Shape, OutExtra>;
"~input": $InferObjectInput<Shape, InExtra>;
"~output": $InferObjectOutput<Shape, Config["out"]>;
"~input": $InferObjectInput<Shape, Config["in"]>;
}
export type $ZodLooseShape = Record<string, any>;

Expand All @@ -1587,26 +1585,25 @@ type OptionalInSchema = { _zod: { optionality: "defaulted" | "optional" } };

export type $InferObjectOutput<T extends $ZodLooseShape, Extra extends Record<string, unknown>> = string extends keyof T
? object
: keyof T extends never
: keyof (T & Extra) extends never
? Record<string, never>
: util.Prettify<
{
-readonly [k in keyof T as T[k] extends OptionalOutSchema ? never : k]: core.output<T[k]>;
} & {
-readonly [k in keyof T as T[k] extends OptionalOutSchema ? k : never]?: core.output<T[k]>;
}
> &
Extra;
} & Extra
>;

export type $InferObjectInput<T extends $ZodLooseShape, Extra extends Record<string, unknown>> = string extends keyof T
? object
: keyof T extends never
: keyof (T & Extra) extends never
? Record<string, never>
: util.Prettify<
{
[k in keyof T as T[k] extends OptionalInSchema ? never : k]: core.input<T[k]>;
-readonly [k in keyof T as T[k] extends OptionalInSchema ? never : k]: core.input<T[k]>;
} & {
[k in keyof T as T[k] extends OptionalInSchema ? k : never]?: core.input<T[k]>;
-readonly [k in keyof T as T[k] extends OptionalInSchema ? k : never]?: core.input<T[k]>;
} & Extra
>;

Expand Down Expand Up @@ -1641,13 +1638,32 @@ function handleOptionalObjectResult(result: ParsePayload, final: ParsePayload, k
}
}

export type $ZodObjectConfig = { out: Record<string, unknown>; in: Record<string, unknown> };

export type $loose = {
out: Record<string, unknown>;
in: Record<string, unknown>;
};
export type $strict = {
out: {};
in: {};
};
export type $strip = {
out: {};
in: {};
};
export type $catchall<T extends $ZodType> = {
out: { [k: string]: core.output<T> };
in: { [k: string]: core.input<T> };
};
export interface $ZodObject<
// @ts-ignore Cast variance
out Shape extends Readonly<$ZodShape> = Readonly<$ZodShape>,
out OutExtra extends Record<string, unknown> = Record<string, unknown>,
out InExtra extends Record<string, unknown> = Record<string, unknown>,
out Params extends $ZodObjectConfig = $ZodObjectConfig,
// out OutExtra extends Record<string, unknown> = Record<string, unknown>,
// out InExtra extends Record<string, unknown> = Record<string, unknown>,
> extends $ZodType {
_zod: $ZodObjectInternals<Shape, OutExtra, InExtra>;
_zod: $ZodObjectInternals<Shape, Params>;
}

export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$constructor("$ZodObject", (inst, def) => {
Expand Down
Loading