zod 4.3.0 → 4.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -27,17 +27,44 @@ test("object intersection: loose", () => {
27
27
  expect(() => C.parse({ a: "foo" })).toThrow();
28
28
  });
29
29
 
30
- test("object intersection: strict", () => {
30
+ test("object intersection: strict + strip", () => {
31
31
  const A = z.strictObject({ a: z.string() });
32
32
  const B = z.object({ b: z.string() });
33
33
 
34
- const C = z.intersection(A, B); // BaseC.merge(HasID);
34
+ const C = z.intersection(A, B);
35
35
  type C = z.infer<typeof C>;
36
36
  expectTypeOf<C>().toEqualTypeOf<{ a: string } & { b: string }>();
37
- const data = { a: "foo", b: "foo", c: "extra" };
38
37
 
39
- const result = C.safeParse(data);
40
- expect(result.success).toEqual(false);
38
+ // Keys recognized by either side should work
39
+ expect(C.parse({ a: "foo", b: "bar" })).toEqual({ a: "foo", b: "bar" });
40
+
41
+ // Extra keys are stripped (follows strip behavior from B)
42
+ expect(C.parse({ a: "foo", b: "bar", c: "extra" })).toEqual({ a: "foo", b: "bar" });
43
+ });
44
+
45
+ test("object intersection: strict + strict", () => {
46
+ const A = z.strictObject({ a: z.string() });
47
+ const B = z.strictObject({ b: z.string() });
48
+
49
+ const C = z.intersection(A, B);
50
+
51
+ // Keys recognized by either side should work
52
+ expect(C.parse({ a: "foo", b: "bar" })).toEqual({ a: "foo", b: "bar" });
53
+
54
+ // Keys unrecognized by BOTH sides should error
55
+ const result = C.safeParse({ a: "foo", b: "bar", c: "extra" });
56
+ expect(result.error?.issues).toMatchInlineSnapshot(`
57
+ [
58
+ {
59
+ "code": "unrecognized_keys",
60
+ "keys": [
61
+ "c",
62
+ ],
63
+ "message": "Unrecognized key: "c"",
64
+ "path": [],
65
+ },
66
+ ]
67
+ `);
41
68
  });
42
69
 
43
70
  test("deep intersection", () => {
@@ -602,14 +602,31 @@ test("index signature in shape", () => {
602
602
  expectTypeOf<schema>().toEqualTypeOf<Record<string, string>>();
603
603
  });
604
604
 
605
- test("extent() on object with refinements should throw", () => {
605
+ test("extend() on object with refinements should throw when overwriting properties", () => {
606
606
  const schema = z
607
607
  .object({
608
608
  a: z.string(),
609
609
  })
610
610
  .refine(() => true);
611
611
 
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();
613
630
  });
614
631
 
615
632
  test("safeExtend() on object with refinements should not throw", () => {
@@ -2460,12 +2460,39 @@ function mergeValues(
2460
2460
  }
2461
2461
 
2462
2462
  function handleIntersectionResults(result: ParsePayload, left: ParsePayload, right: ParsePayload): ParsePayload {
2463
- if (left.issues.length) {
2464
- result.issues.push(...left.issues);
2463
+ // Track which side(s) report each key as unrecognized
2464
+ const unrecKeys = new Map<string, { l?: true; r?: true }>();
2465
+ let unrecIssue: errors.$ZodRawIssue | undefined;
2466
+
2467
+ for (const iss of left.issues) {
2468
+ if (iss.code === "unrecognized_keys") {
2469
+ unrecIssue ??= iss;
2470
+ for (const k of iss.keys) {
2471
+ if (!unrecKeys.has(k)) unrecKeys.set(k, {});
2472
+ unrecKeys.get(k)!.l = true;
2473
+ }
2474
+ } else {
2475
+ result.issues.push(iss);
2476
+ }
2465
2477
  }
2466
- if (right.issues.length) {
2467
- result.issues.push(...right.issues);
2478
+
2479
+ for (const iss of right.issues) {
2480
+ if (iss.code === "unrecognized_keys") {
2481
+ for (const k of iss.keys) {
2482
+ if (!unrecKeys.has(k)) unrecKeys.set(k, {});
2483
+ unrecKeys.get(k)!.r = true;
2484
+ }
2485
+ } else {
2486
+ result.issues.push(iss);
2487
+ }
2468
2488
  }
2489
+
2490
+ // Report only keys unrecognized by BOTH sides
2491
+ const bothKeys = [...unrecKeys].filter(([, f]) => f.l && f.r).map(([k]) => k);
2492
+ if (bothKeys.length && unrecIssue) {
2493
+ result.issues.push({ ...unrecIssue, keys: bothKeys });
2494
+ }
2495
+
2469
2496
  if (util.aborted(result)) return result;
2470
2497
 
2471
2498
  const merged = mergeValues(left.value, right.value);
@@ -656,7 +656,14 @@ export function extend(schema: schemas.$ZodObject, shape: schemas.$ZodShape): an
656
656
  const checks = schema._zod.def.checks;
657
657
  const hasChecks = checks && checks.length > 0;
658
658
  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
+ }
660
667
  }
661
668
 
662
669
  const def = mergeDefs(schema._zod.def, {
@@ -665,7 +672,6 @@ export function extend(schema: schemas.$ZodObject, shape: schemas.$ZodShape): an
665
672
  assignProp(this, "shape", _shape); // self-caching
666
673
  return _shape;
667
674
  },
668
- checks: [],
669
675
  });
670
676
  return clone(schema, def) as any;
671
677
  }
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 3,
4
- patch: 0 as number,
4
+ patch: 2 as number,
5
5
  } as const;
@@ -1214,11 +1214,38 @@ function mergeValues(a, b) {
1214
1214
  return { valid: false, mergeErrorPath: [] };
1215
1215
  }
1216
1216
  function handleIntersectionResults(result, left, right) {
1217
- if (left.issues.length) {
1218
- result.issues.push(...left.issues);
1217
+ // Track which side(s) report each key as unrecognized
1218
+ const unrecKeys = new Map();
1219
+ let unrecIssue;
1220
+ for (const iss of left.issues) {
1221
+ if (iss.code === "unrecognized_keys") {
1222
+ unrecIssue ?? (unrecIssue = iss);
1223
+ for (const k of iss.keys) {
1224
+ if (!unrecKeys.has(k))
1225
+ unrecKeys.set(k, {});
1226
+ unrecKeys.get(k).l = true;
1227
+ }
1228
+ }
1229
+ else {
1230
+ result.issues.push(iss);
1231
+ }
1232
+ }
1233
+ for (const iss of right.issues) {
1234
+ if (iss.code === "unrecognized_keys") {
1235
+ for (const k of iss.keys) {
1236
+ if (!unrecKeys.has(k))
1237
+ unrecKeys.set(k, {});
1238
+ unrecKeys.get(k).r = true;
1239
+ }
1240
+ }
1241
+ else {
1242
+ result.issues.push(iss);
1243
+ }
1219
1244
  }
1220
- if (right.issues.length) {
1221
- result.issues.push(...right.issues);
1245
+ // Report only keys unrecognized by BOTH sides
1246
+ const bothKeys = [...unrecKeys].filter(([, f]) => f.l && f.r).map(([k]) => k);
1247
+ if (bothKeys.length && unrecIssue) {
1248
+ result.issues.push({ ...unrecIssue, keys: bothKeys });
1222
1249
  }
1223
1250
  if (util.aborted(result))
1224
1251
  return result;
@@ -1183,11 +1183,38 @@ function mergeValues(a, b) {
1183
1183
  return { valid: false, mergeErrorPath: [] };
1184
1184
  }
1185
1185
  function handleIntersectionResults(result, left, right) {
1186
- if (left.issues.length) {
1187
- result.issues.push(...left.issues);
1186
+ // Track which side(s) report each key as unrecognized
1187
+ const unrecKeys = new Map();
1188
+ let unrecIssue;
1189
+ for (const iss of left.issues) {
1190
+ if (iss.code === "unrecognized_keys") {
1191
+ unrecIssue ?? (unrecIssue = iss);
1192
+ for (const k of iss.keys) {
1193
+ if (!unrecKeys.has(k))
1194
+ unrecKeys.set(k, {});
1195
+ unrecKeys.get(k).l = true;
1196
+ }
1197
+ }
1198
+ else {
1199
+ result.issues.push(iss);
1200
+ }
1201
+ }
1202
+ for (const iss of right.issues) {
1203
+ if (iss.code === "unrecognized_keys") {
1204
+ for (const k of iss.keys) {
1205
+ if (!unrecKeys.has(k))
1206
+ unrecKeys.set(k, {});
1207
+ unrecKeys.get(k).r = true;
1208
+ }
1209
+ }
1210
+ else {
1211
+ result.issues.push(iss);
1212
+ }
1188
1213
  }
1189
- if (right.issues.length) {
1190
- result.issues.push(...right.issues);
1214
+ // Report only keys unrecognized by BOTH sides
1215
+ const bothKeys = [...unrecKeys].filter(([, f]) => f.l && f.r).map(([k]) => k);
1216
+ if (bothKeys.length && unrecIssue) {
1217
+ result.issues.push({ ...unrecIssue, keys: bothKeys });
1191
1218
  }
1192
1219
  if (util.aborted(result))
1193
1220
  return result;
package/v4/core/util.cjs CHANGED
@@ -440,7 +440,14 @@ function extend(schema, shape) {
440
440
  const checks = schema._zod.def.checks;
441
441
  const hasChecks = checks && checks.length > 0;
442
442
  if (hasChecks) {
443
- throw new Error("Object schemas containing refinements cannot be extended. Use `.safeExtend()` instead.");
443
+ // Only throw if new shape overlaps with existing shape
444
+ // Use getOwnPropertyDescriptor to check key existence without accessing values
445
+ const existingShape = schema._zod.def.shape;
446
+ for (const key in shape) {
447
+ if (Object.getOwnPropertyDescriptor(existingShape, key) !== undefined) {
448
+ throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
449
+ }
450
+ }
444
451
  }
445
452
  const def = mergeDefs(schema._zod.def, {
446
453
  get shape() {
@@ -448,7 +455,6 @@ function extend(schema, shape) {
448
455
  assignProp(this, "shape", _shape); // self-caching
449
456
  return _shape;
450
457
  },
451
- checks: [],
452
458
  });
453
459
  return clone(schema, def);
454
460
  }
package/v4/core/util.js CHANGED
@@ -382,7 +382,14 @@ export function extend(schema, shape) {
382
382
  const checks = schema._zod.def.checks;
383
383
  const hasChecks = checks && checks.length > 0;
384
384
  if (hasChecks) {
385
- throw new Error("Object schemas containing refinements cannot be extended. Use `.safeExtend()` instead.");
385
+ // Only throw if new shape overlaps with existing shape
386
+ // Use getOwnPropertyDescriptor to check key existence without accessing values
387
+ const existingShape = schema._zod.def.shape;
388
+ for (const key in shape) {
389
+ if (Object.getOwnPropertyDescriptor(existingShape, key) !== undefined) {
390
+ throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
391
+ }
392
+ }
386
393
  }
387
394
  const def = mergeDefs(schema._zod.def, {
388
395
  get shape() {
@@ -390,7 +397,6 @@ export function extend(schema, shape) {
390
397
  assignProp(this, "shape", _shape); // self-caching
391
398
  return _shape;
392
399
  },
393
- checks: [],
394
400
  });
395
401
  return clone(schema, def);
396
402
  }
@@ -4,5 +4,5 @@ exports.version = void 0;
4
4
  exports.version = {
5
5
  major: 4,
6
6
  minor: 3,
7
- patch: 0,
7
+ patch: 2,
8
8
  };
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 3,
4
- patch: 0,
4
+ patch: 2,
5
5
  };