Skip to content

Commit 0fe8840

Browse files
committed
allow non-overwriting extends with refinements. 4.3.1
1 parent 1899684 commit 0fe8840

File tree

6 files changed

+32
-11
lines changed

6 files changed

+32
-11
lines changed

packages/zod/jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zod/zod",
3-
"version": "4.3.0",
3+
"version": "4.3.1",
44
"exports": {
55
"./package.json": "./package.json",
66
".": "./src/index.ts",

packages/zod/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zod",
3-
"version": "4.3.0",
3+
"version": "4.3.1",
44
"type": "module",
55
"license": "MIT",
66
"author": "Colin McDonnell <zod@colinhacks.com>",

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -602,14 +602,31 @@ test("index signature in shape", () => {
602602
expectTypeOf<schema>().toEqualTypeOf<Record<string, string>>();
603603
});
604604

605-
test("extent() on object with refinements should throw", () => {
605+
test("extend() on object with refinements should throw when overwriting properties", () => {
606606
const schema = z
607607
.object({
608608
a: z.string(),
609609
})
610610
.refine(() => true);
611611

612-
expect(() => schema.extend({ b: z.string() })).toThrow();
612+
expect(() => schema.extend({ a: z.number() })).toThrow();
613+
});
614+
615+
test("extend() on object with refinements should not throw when adding new properties", () => {
616+
const schema = z
617+
.object({
618+
a: z.string(),
619+
})
620+
.refine((data) => data.a.length > 0);
621+
622+
// Should not throw since 'b' doesn't overlap with 'a'
623+
const extended = schema.extend({ b: z.number() });
624+
625+
// Verify the extended schema works correctly
626+
expect(extended.parse({ a: "hello", b: 42 })).toEqual({ a: "hello", b: 42 });
627+
628+
// Verify the original refinement still applies
629+
expect(() => extended.parse({ a: "", b: 42 })).toThrow();
613630
});
614631

615632
test("safeExtend() on object with refinements should not throw", () => {

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,14 @@ export function extend(schema: schemas.$ZodObject, shape: schemas.$ZodShape): an
656656
const checks = schema._zod.def.checks;
657657
const hasChecks = checks && checks.length > 0;
658658
if (hasChecks) {
659-
throw new Error("Object schemas containing refinements cannot be extended. Use `.safeExtend()` instead.");
659+
// Only throw if new shape overlaps with existing shape
660+
// Use getOwnPropertyDescriptor to check key existence without accessing values
661+
const existingShape = schema._zod.def.shape;
662+
for (const key in shape) {
663+
if (Object.getOwnPropertyDescriptor(existingShape, key) !== undefined) {
664+
throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
665+
}
666+
}
660667
}
661668

662669
const def = mergeDefs(schema._zod.def, {
@@ -665,7 +672,6 @@ export function extend(schema: schemas.$ZodObject, shape: schemas.$ZodShape): an
665672
assignProp(this, "shape", _shape); // self-caching
666673
return _shape;
667674
},
668-
checks: [],
669675
});
670676
return clone(schema, def) as any;
671677
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export const version = {
22
major: 4,
33
minor: 3,
4-
patch: 0 as number,
4+
patch: 1 as number,
55
} as const;

play.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import * as z from "./packages/zod/src/v4/index.js";
22

3-
const A = z.object({
4-
a: z.string().optional(),
5-
});
3+
const Schema = z.record(z.enum(["key1", "key2"]), z.number());
64

7-
console.log(A.parse({}));
5+
console.log(z.toJSONSchema(Schema));

0 commit comments

Comments
 (0)