Skip to content

Commit ffff1aa

Browse files
committed
Clone POJO objects during defaulting/prefaulting
1 parent 6887ff3 commit ffff1aa

File tree

7 files changed

+47
-5
lines changed

7 files changed

+47
-5
lines changed

packages/zod/src/v4/classic/schemas.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,7 +1688,7 @@ export function _default<T extends core.SomeType>(
16881688
type: "default",
16891689
innerType: innerType as any as core.$ZodType,
16901690
get defaultValue() {
1691-
return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue;
1691+
return typeof defaultValue === "function" ? (defaultValue as Function)() : util.shallowClone(defaultValue);
16921692
},
16931693
}) as any;
16941694
}
@@ -1716,7 +1716,7 @@ export function prefault<T extends core.SomeType>(
17161716
type: "prefault",
17171717
innerType: innerType as any as core.$ZodType,
17181718
get defaultValue() {
1719-
return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue;
1719+
return typeof defaultValue === "function" ? (defaultValue as Function)() : util.shallowClone(defaultValue);
17201720
},
17211721
}) as any;
17221722
}

packages/zod/src/v4/classic/tests/default.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,15 @@ test("partial should not clobber defaults", () => {
311311
}
312312
`);
313313
});
314+
315+
test("defaulted object schema returns shallow clone", () => {
316+
const schema = z
317+
.object({
318+
a: z.string(),
319+
})
320+
.default({ a: "x" });
321+
const result1 = schema.parse(undefined);
322+
const result2 = schema.parse(undefined);
323+
expect(result1).not.toBe(result2);
324+
expect(result1).toEqual(result2);
325+
});

packages/zod/src/v4/classic/tests/prefault.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,15 @@ test("prefault inside object", () => {
3535
email: string;
3636
}>();
3737
});
38+
39+
test("object schema with prefault should return shallow clone", () => {
40+
const schema = z
41+
.object({
42+
a: z.string(),
43+
})
44+
.default({ a: "x" });
45+
const result1 = schema.parse(undefined);
46+
const result2 = schema.parse(undefined);
47+
expect(result1).not.toBe(result2);
48+
expect(result1).toEqual(result2);
49+
});

packages/zod/src/v4/core/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1321,7 +1321,7 @@ export function _default<T extends schemas.$ZodObject>(
13211321
type: "default",
13221322
innerType,
13231323
get defaultValue() {
1324-
return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue;
1324+
return typeof defaultValue === "function" ? (defaultValue as Function)() : util.shallowClone(defaultValue);
13251325
},
13261326
}) as any;
13271327
}

packages/zod/src/v4/core/util.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,11 @@ export function isPlainObject(o: any): o is Record<PropertyKey, unknown> {
389389
return true;
390390
}
391391

392+
export function shallowClone(o: any): any {
393+
if (isPlainObject(o)) return { ...o };
394+
return o;
395+
}
396+
392397
export function numKeys(data: any): number {
393398
let keyCount = 0;
394399
for (const key in data) {

packages/zod/src/v4/mini/schemas.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,7 +1256,7 @@ export function _default<T extends SomeType>(
12561256
type: "default",
12571257
innerType: innerType as any as core.$ZodType,
12581258
get defaultValue() {
1259-
return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue;
1259+
return typeof defaultValue === "function" ? (defaultValue as Function)() : util.shallowClone(defaultValue);
12601260
},
12611261
}) as any;
12621262
}
@@ -1281,7 +1281,7 @@ export function prefault<T extends SomeType>(
12811281
type: "prefault",
12821282
innerType: innerType as any as core.$ZodType,
12831283
get defaultValue() {
1284-
return typeof defaultValue === "function" ? (defaultValue as Function)() : defaultValue;
1284+
return typeof defaultValue === "function" ? (defaultValue as Function)() : util.shallowClone(defaultValue);
12851285
},
12861286
}) as any;
12871287
}

packages/zod/src/v4/mini/tests/index.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,3 +869,16 @@ test("def typing", () => {
869869
z.catch(z.string(), "fallback").def.type satisfies "catch";
870870
z.file().def.type satisfies "file";
871871
});
872+
873+
test("defaulted object schema returns shallow clone", () => {
874+
const schema = z._default(
875+
z.object({
876+
a: z.string(),
877+
}),
878+
{ a: "x" }
879+
);
880+
const result1 = schema.parse(undefined);
881+
const result2 = schema.parse(undefined);
882+
expect(result1).not.toBe(result2);
883+
expect(result1).toEqual(result2);
884+
});

0 commit comments

Comments
 (0)