zod 4.2.0-canary.20251118T062010 → 4.2.0-canary.20251118T185426

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.2.0-canary.20251118T062010",
3
+ "version": "4.2.0-canary.20251118T185426",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -302,6 +302,29 @@ test("z.record", () => {
302
302
  const d = z.record(z.enum(["a", "b"]).or(z.never()), z.string());
303
303
  type d = z.output<typeof d>;
304
304
  expectTypeOf<d>().toEqualTypeOf<Record<"a" | "b", string>>();
305
+
306
+ // literal union keys
307
+ const e = z.record(z.union([z.literal("a"), z.literal(0)]), z.string());
308
+ type e = z.output<typeof e>;
309
+ expectTypeOf<e>().toEqualTypeOf<Record<"a" | 0, string>>();
310
+ expect(z.parse(e, { a: "hello", 0: "world" })).toEqual({
311
+ a: "hello",
312
+ 0: "world",
313
+ });
314
+
315
+ // TypeScript enum keys
316
+ enum Enum {
317
+ A = 0,
318
+ B = "hi",
319
+ }
320
+
321
+ const f = z.record(z.enum(Enum), z.string());
322
+ type f = z.output<typeof f>;
323
+ expectTypeOf<f>().toEqualTypeOf<Record<Enum, string>>();
324
+ expect(z.parse(f, { [Enum.A]: "hello", [Enum.B]: "world" })).toEqual({
325
+ [Enum.A]: "hello",
326
+ [Enum.B]: "world",
327
+ });
305
328
  });
306
329
 
307
330
  test("z.map", () => {
@@ -786,6 +809,37 @@ test("isPlainObject", () => {
786
809
  expect(z.core.util.isPlainObject("string")).toEqual(false);
787
810
  expect(z.core.util.isPlainObject(123)).toEqual(false);
788
811
  expect(z.core.util.isPlainObject(Symbol())).toEqual(false);
812
+ expect(z.core.util.isPlainObject({ constructor: "string" })).toEqual(true);
813
+ expect(z.core.util.isPlainObject({ constructor: 123 })).toEqual(true);
814
+ expect(z.core.util.isPlainObject({ constructor: null })).toEqual(true);
815
+ expect(z.core.util.isPlainObject({ constructor: undefined })).toEqual(true);
816
+ expect(z.core.util.isPlainObject({ constructor: true })).toEqual(true);
817
+ expect(z.core.util.isPlainObject({ constructor: {} })).toEqual(true);
818
+ expect(z.core.util.isPlainObject({ constructor: [] })).toEqual(true);
819
+ });
820
+
821
+ test("shallowClone with constructor field", () => {
822
+ const objWithConstructor = { constructor: "string", key: "value" };
823
+ const cloned = z.core.util.shallowClone(objWithConstructor);
824
+
825
+ expect(cloned).toEqual(objWithConstructor);
826
+ expect(cloned).not.toBe(objWithConstructor);
827
+ expect(cloned.constructor).toBe("string");
828
+ expect(cloned.key).toBe("value");
829
+
830
+ const testCases = [
831
+ { constructor: 123, data: "test" },
832
+ { constructor: null, data: "test" },
833
+ { constructor: true, data: "test" },
834
+ { constructor: {}, data: "test" },
835
+ { constructor: [], data: "test" },
836
+ ];
837
+
838
+ for (const testCase of testCases) {
839
+ const clonedCase = z.core.util.shallowClone(testCase);
840
+ expect(clonedCase).toEqual(testCase);
841
+ expect(clonedCase).not.toBe(testCase);
842
+ }
789
843
  });
790
844
 
791
845
  test("def typing", () => {
@@ -8,16 +8,28 @@ test("type inference", () => {
8
8
  const recordWithEnumKeys = z.record(z.enum(["Tuna", "Salmon"]), z.string());
9
9
  type recordWithEnumKeys = z.infer<typeof recordWithEnumKeys>;
10
10
 
11
- const recordWithLiteralKey = z.record(z.literal(["Tuna", "Salmon"]), z.string());
11
+ const recordWithLiteralKey = z.record(z.literal(["Tuna", "Salmon", 21]), z.string());
12
12
  type recordWithLiteralKey = z.infer<typeof recordWithLiteralKey>;
13
13
 
14
- const recordWithLiteralUnionKeys = z.record(z.union([z.literal("Tuna"), z.literal("Salmon")]), z.string());
14
+ const recordWithLiteralUnionKeys = z.record(
15
+ z.union([z.literal("Tuna"), z.literal("Salmon"), z.literal(21)]),
16
+ z.string()
17
+ );
15
18
  type recordWithLiteralUnionKeys = z.infer<typeof recordWithLiteralUnionKeys>;
16
19
 
20
+ enum Enum {
21
+ Tuna = 0,
22
+ Salmon = "Shark",
23
+ }
24
+
25
+ const recordWithTypescriptEnum = z.record(z.enum(Enum), z.string());
26
+ type recordWithTypescriptEnum = z.infer<typeof recordWithTypescriptEnum>;
27
+
17
28
  expectTypeOf<booleanRecord>().toEqualTypeOf<Record<string, boolean>>();
18
29
  expectTypeOf<recordWithEnumKeys>().toEqualTypeOf<Record<"Tuna" | "Salmon", string>>();
19
- expectTypeOf<recordWithLiteralKey>().toEqualTypeOf<Record<"Tuna" | "Salmon", string>>();
20
- expectTypeOf<recordWithLiteralUnionKeys>().toEqualTypeOf<Record<"Tuna" | "Salmon", string>>();
30
+ expectTypeOf<recordWithLiteralKey>().toEqualTypeOf<Record<"Tuna" | "Salmon" | 21, string>>();
31
+ expectTypeOf<recordWithLiteralUnionKeys>().toEqualTypeOf<Record<"Tuna" | "Salmon" | 21, string>>();
32
+ expectTypeOf<recordWithTypescriptEnum>().toEqualTypeOf<Record<Enum, string>>();
21
33
  });
22
34
 
23
35
  test("enum exhaustiveness", () => {
@@ -64,14 +76,76 @@ test("enum exhaustiveness", () => {
64
76
  `);
65
77
  });
66
78
 
79
+ test("typescript enum exhaustiveness", () => {
80
+ enum BigFish {
81
+ Tuna = 0,
82
+ Salmon = "Shark",
83
+ }
84
+
85
+ const schema = z.record(z.enum(BigFish), z.string());
86
+ const value = {
87
+ [BigFish.Tuna]: "asdf",
88
+ [BigFish.Salmon]: "asdf",
89
+ };
90
+
91
+ expect(schema.parse(value)).toEqual(value);
92
+
93
+ expect(schema.safeParse({ [BigFish.Tuna]: "asdf", [BigFish.Salmon]: "asdf", Trout: "asdf" })).toMatchInlineSnapshot(`
94
+ {
95
+ "error": [ZodError: [
96
+ {
97
+ "code": "unrecognized_keys",
98
+ "keys": [
99
+ "Trout"
100
+ ],
101
+ "path": [],
102
+ "message": "Unrecognized key: \\"Trout\\""
103
+ }
104
+ ]],
105
+ "success": false,
106
+ }
107
+ `);
108
+ expect(schema.safeParse({ [BigFish.Tuna]: "asdf" })).toMatchInlineSnapshot(`
109
+ {
110
+ "error": [ZodError: [
111
+ {
112
+ "expected": "string",
113
+ "code": "invalid_type",
114
+ "path": [
115
+ "Shark"
116
+ ],
117
+ "message": "Invalid input: expected string, received undefined"
118
+ }
119
+ ]],
120
+ "success": false,
121
+ }
122
+ `);
123
+ expect(schema.safeParse({ [BigFish.Salmon]: "asdf" })).toMatchInlineSnapshot(`
124
+ {
125
+ "error": [ZodError: [
126
+ {
127
+ "expected": "string",
128
+ "code": "invalid_type",
129
+ "path": [
130
+ 0
131
+ ],
132
+ "message": "Invalid input: expected string, received undefined"
133
+ }
134
+ ]],
135
+ "success": false,
136
+ }
137
+ `);
138
+ });
139
+
67
140
  test("literal exhaustiveness", () => {
68
- const schema = z.record(z.literal(["Tuna", "Salmon"]), z.string());
141
+ const schema = z.record(z.literal(["Tuna", "Salmon", 21]), z.string());
69
142
  schema.parse({
70
143
  Tuna: "asdf",
71
144
  Salmon: "asdf",
145
+ 21: "asdf",
72
146
  });
73
147
 
74
- expect(schema.safeParse({ Tuna: "asdf", Salmon: "asdf", Trout: "asdf" })).toMatchInlineSnapshot(`
148
+ expect(schema.safeParse({ Tuna: "asdf", Salmon: "asdf", 21: "asdf", Trout: "asdf" })).toMatchInlineSnapshot(`
75
149
  {
76
150
  "error": [ZodError: [
77
151
  {
@@ -96,6 +170,14 @@ test("literal exhaustiveness", () => {
96
170
  "Salmon"
97
171
  ],
98
172
  "message": "Invalid input: expected string, received undefined"
173
+ },
174
+ {
175
+ "expected": "string",
176
+ "code": "invalid_type",
177
+ "path": [
178
+ 21
179
+ ],
180
+ "message": "Invalid input: expected string, received undefined"
99
181
  }
100
182
  ]],
101
183
  "success": false,
@@ -143,13 +225,14 @@ test("pipe exhaustiveness", () => {
143
225
  });
144
226
 
145
227
  test("union exhaustiveness", () => {
146
- const schema = z.record(z.union([z.literal("Tuna"), z.literal("Salmon")]), z.string());
147
- expect(schema.parse({ Tuna: "asdf", Salmon: "asdf" })).toEqual({
228
+ const schema = z.record(z.union([z.literal("Tuna"), z.literal("Salmon"), z.literal(21)]), z.string());
229
+ expect(schema.parse({ Tuna: "asdf", Salmon: "asdf", 21: "asdf" })).toEqual({
148
230
  Tuna: "asdf",
149
231
  Salmon: "asdf",
232
+ 21: "asdf",
150
233
  });
151
234
 
152
- expect(schema.safeParse({ Tuna: "asdf", Salmon: "asdf", Trout: "asdf" })).toMatchInlineSnapshot(`
235
+ expect(schema.safeParse({ Tuna: "asdf", Salmon: "asdf", 21: "asdf", Trout: "asdf" })).toMatchInlineSnapshot(`
153
236
  {
154
237
  "error": [ZodError: [
155
238
  {
@@ -174,6 +257,14 @@ test("union exhaustiveness", () => {
174
257
  "Salmon"
175
258
  ],
176
259
  "message": "Invalid input: expected string, received undefined"
260
+ },
261
+ {
262
+ "expected": "string",
263
+ "code": "invalid_type",
264
+ "path": [
265
+ 21
266
+ ],
267
+ "message": "Invalid input: expected string, received undefined"
177
268
  }
178
269
  ]],
179
270
  "success": false,
@@ -2603,11 +2603,13 @@ export const $ZodRecord: core.$constructor<$ZodRecord> = /*@__PURE__*/ core.$con
2603
2603
 
2604
2604
  const proms: Promise<any>[] = [];
2605
2605
 
2606
- if (def.keyType._zod.values) {
2607
- const values = def.keyType._zod.values!;
2606
+ const values = def.keyType._zod.values;
2607
+ if (values) {
2608
2608
  payload.value = {};
2609
+ const recordKeys = new Set<string | symbol>();
2609
2610
  for (const key of values) {
2610
2611
  if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
2612
+ recordKeys.add(typeof key === "number" ? key.toString() : key);
2611
2613
  const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
2612
2614
 
2613
2615
  if (result instanceof Promise) {
@@ -2630,7 +2632,7 @@ export const $ZodRecord: core.$constructor<$ZodRecord> = /*@__PURE__*/ core.$con
2630
2632
 
2631
2633
  let unrecognized!: string[];
2632
2634
  for (const key in input) {
2633
- if (!values.has(key)) {
2635
+ if (!recordKeys.has(key)) {
2634
2636
  unrecognized = unrecognized ?? [];
2635
2637
  unrecognized.push(key);
2636
2638
  }
@@ -1,4 +1,4 @@
1
- import { test } from "vitest";
1
+ import { expect, test } from "vitest";
2
2
  import * as z from "zod/v4";
3
3
 
4
4
  test("extend chaining preserves and overrides properties", () => {
@@ -16,3 +16,44 @@ test("extend chaining preserves and overrides properties", () => {
16
16
 
17
17
  schema3.parse({ email: "test@example.com" });
18
18
  });
19
+
20
+ test("extend with constructor field in shape", () => {
21
+ const baseSchema = z.object({
22
+ name: z.string(),
23
+ });
24
+
25
+ const extendedSchema = baseSchema.extend({
26
+ constructor: z.string(),
27
+ age: z.number(),
28
+ });
29
+
30
+ const result = extendedSchema.parse({
31
+ name: "John",
32
+ constructor: "Person",
33
+ age: 30,
34
+ });
35
+
36
+ expect(result).toEqual({
37
+ name: "John",
38
+ constructor: "Person",
39
+ age: 30,
40
+ });
41
+
42
+ const testCases = [
43
+ { name: "Test", constructor: 123, age: 25 },
44
+ { name: "Test", constructor: null, age: 25 },
45
+ { name: "Test", constructor: true, age: 25 },
46
+ { name: "Test", constructor: {}, age: 25 },
47
+ ];
48
+
49
+ for (const testCase of testCases) {
50
+ const anyConstructorSchema = baseSchema.extend({
51
+ constructor: z.any(),
52
+ age: z.number(),
53
+ });
54
+
55
+ expect(() => anyConstructorSchema.parse(testCase)).not.toThrow();
56
+ const parsed = anyConstructorSchema.parse(testCase);
57
+ expect(parsed).toEqual(testCase);
58
+ }
59
+ });
@@ -0,0 +1,67 @@
1
+ import { expect, test } from "vitest";
2
+ import * as z from "zod/v4";
3
+
4
+ test("record should parse objects with non-function constructor field", () => {
5
+ const schema = z.record(z.string(), z.any());
6
+
7
+ expect(() => schema.parse({ constructor: "string", key: "value" })).not.toThrow();
8
+
9
+ const result1 = schema.parse({ constructor: "string", key: "value" });
10
+ expect(result1).toEqual({ constructor: "string", key: "value" });
11
+
12
+ expect(() => schema.parse({ constructor: 123, key: "value" })).not.toThrow();
13
+
14
+ const result2 = schema.parse({ constructor: 123, key: "value" });
15
+ expect(result2).toEqual({ constructor: 123, key: "value" });
16
+
17
+ expect(() => schema.parse({ constructor: null, key: "value" })).not.toThrow();
18
+
19
+ const result3 = schema.parse({ constructor: null, key: "value" });
20
+ expect(result3).toEqual({ constructor: null, key: "value" });
21
+
22
+ expect(() => schema.parse({ constructor: {}, key: "value" })).not.toThrow();
23
+
24
+ const result4 = schema.parse({ constructor: {}, key: "value" });
25
+ expect(result4).toEqual({ constructor: {}, key: "value" });
26
+
27
+ expect(() => schema.parse({ constructor: [], key: "value" })).not.toThrow();
28
+
29
+ const result5 = schema.parse({ constructor: [], key: "value" });
30
+ expect(result5).toEqual({ constructor: [], key: "value" });
31
+
32
+ expect(() => schema.parse({ constructor: true, key: "value" })).not.toThrow();
33
+
34
+ const result6 = schema.parse({ constructor: true, key: "value" });
35
+ expect(result6).toEqual({ constructor: true, key: "value" });
36
+ });
37
+
38
+ test("record should still work with normal objects", () => {
39
+ const schema = z.record(z.string(), z.string());
40
+
41
+ expect(() => schema.parse({ normalKey: "value" })).not.toThrow();
42
+
43
+ const result1 = schema.parse({ normalKey: "value" });
44
+ expect(result1).toEqual({ normalKey: "value" });
45
+
46
+ expect(() => schema.parse({ key1: "value1", key2: "value2" })).not.toThrow();
47
+
48
+ const result2 = schema.parse({ key1: "value1", key2: "value2" });
49
+ expect(result2).toEqual({ key1: "value1", key2: "value2" });
50
+ });
51
+
52
+ test("record should validate values according to schema even with constructor field", () => {
53
+ const stringSchema = z.record(z.string(), z.string());
54
+
55
+ expect(() => stringSchema.parse({ constructor: "string", key: "value" })).not.toThrow();
56
+
57
+ expect(() => stringSchema.parse({ constructor: 123, key: "value" })).toThrow();
58
+ });
59
+
60
+ test("record should work with different key types and constructor field", () => {
61
+ const enumSchema = z.record(z.enum(["constructor", "key"]), z.string());
62
+
63
+ expect(() => enumSchema.parse({ constructor: "value1", key: "value2" })).not.toThrow();
64
+
65
+ const result = enumSchema.parse({ constructor: "value1", key: "value2" });
66
+ expect(result).toEqual({ constructor: "value1", key: "value2" });
67
+ });
@@ -381,6 +381,8 @@ export function isPlainObject(o: any): o is Record<PropertyKey, unknown> {
381
381
  const ctor = o.constructor;
382
382
  if (ctor === undefined) return true;
383
383
 
384
+ if (typeof ctor !== "function") return true;
385
+
384
386
  // modified prototype
385
387
  const prot = ctor.prototype;
386
388
  if (isObject(prot) === false) return false;
@@ -296,6 +296,29 @@ test("z.record", () => {
296
296
  expect(() => z.parse(c, { a: "hello", b: "world" })).toThrow();
297
297
  // extra keys
298
298
  expect(() => z.parse(c, { a: "hello", b: "world", c: "world", d: "world" })).toThrow();
299
+
300
+ // literal union keys
301
+ const d = z.record(z.union([z.literal("a"), z.literal(0)]), z.string());
302
+ type d = z.output<typeof d>;
303
+ expectTypeOf<d>().toEqualTypeOf<Record<"a" | 0, string>>();
304
+ expect(z.parse(d, { a: "hello", 0: "world" })).toEqual({
305
+ a: "hello",
306
+ 0: "world",
307
+ });
308
+
309
+ // TypeScript enum keys
310
+ enum Enum {
311
+ A = 0,
312
+ B = "hi",
313
+ }
314
+
315
+ const e = z.record(z.enum(Enum), z.string());
316
+ type e = z.output<typeof e>;
317
+ expectTypeOf<e>().toEqualTypeOf<Record<Enum, string>>();
318
+ expect(z.parse(e, { [Enum.A]: "hello", [Enum.B]: "world" })).toEqual({
319
+ [Enum.A]: "hello",
320
+ [Enum.B]: "world",
321
+ });
299
322
  });
300
323
 
301
324
  test("z.map", () => {
@@ -1237,11 +1237,13 @@ exports.$ZodRecord = core.$constructor("$ZodRecord", (inst, def) => {
1237
1237
  return payload;
1238
1238
  }
1239
1239
  const proms = [];
1240
- if (def.keyType._zod.values) {
1241
- const values = def.keyType._zod.values;
1240
+ const values = def.keyType._zod.values;
1241
+ if (values) {
1242
1242
  payload.value = {};
1243
+ const recordKeys = new Set();
1243
1244
  for (const key of values) {
1244
1245
  if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
1246
+ recordKeys.add(typeof key === "number" ? key.toString() : key);
1245
1247
  const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
1246
1248
  if (result instanceof Promise) {
1247
1249
  proms.push(result.then((result) => {
@@ -1261,7 +1263,7 @@ exports.$ZodRecord = core.$constructor("$ZodRecord", (inst, def) => {
1261
1263
  }
1262
1264
  let unrecognized;
1263
1265
  for (const key in input) {
1264
- if (!values.has(key)) {
1266
+ if (!recordKeys.has(key)) {
1265
1267
  unrecognized = unrecognized ?? [];
1266
1268
  unrecognized.push(key);
1267
1269
  }
@@ -1206,11 +1206,13 @@ export const $ZodRecord = /*@__PURE__*/ core.$constructor("$ZodRecord", (inst, d
1206
1206
  return payload;
1207
1207
  }
1208
1208
  const proms = [];
1209
- if (def.keyType._zod.values) {
1210
- const values = def.keyType._zod.values;
1209
+ const values = def.keyType._zod.values;
1210
+ if (values) {
1211
1211
  payload.value = {};
1212
+ const recordKeys = new Set();
1212
1213
  for (const key of values) {
1213
1214
  if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
1215
+ recordKeys.add(typeof key === "number" ? key.toString() : key);
1214
1216
  const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
1215
1217
  if (result instanceof Promise) {
1216
1218
  proms.push(result.then((result) => {
@@ -1230,7 +1232,7 @@ export const $ZodRecord = /*@__PURE__*/ core.$constructor("$ZodRecord", (inst, d
1230
1232
  }
1231
1233
  let unrecognized;
1232
1234
  for (const key in input) {
1233
- if (!values.has(key)) {
1235
+ if (!recordKeys.has(key)) {
1234
1236
  unrecognized = unrecognized ?? [];
1235
1237
  unrecognized.push(key);
1236
1238
  }
package/v4/core/util.cjs CHANGED
@@ -215,6 +215,8 @@ function isPlainObject(o) {
215
215
  const ctor = o.constructor;
216
216
  if (ctor === undefined)
217
217
  return true;
218
+ if (typeof ctor !== "function")
219
+ return true;
218
220
  // modified prototype
219
221
  const prot = ctor.prototype;
220
222
  if (isObject(prot) === false)
package/v4/core/util.js CHANGED
@@ -160,6 +160,8 @@ export function isPlainObject(o) {
160
160
  const ctor = o.constructor;
161
161
  if (ctor === undefined)
162
162
  return true;
163
+ if (typeof ctor !== "function")
164
+ return true;
163
165
  // modified prototype
164
166
  const prot = ctor.prototype;
165
167
  if (isObject(prot) === false)