zod 4.5.3 → 4.5.4

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.5.3",
3
+ "version": "4.5.4",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -676,3 +676,45 @@ test("guards a transform built before any container", () => {
676
676
 
677
677
  expect(() => Wrapped.parse(input)).toThrow(/reference cycle/);
678
678
  });
679
+
680
+ test("detects a cycle reachable only through a merged catchall", () => {
681
+ const Cyclic: any = z.object({
682
+ id: z.string(),
683
+ get next() {
684
+ return z.optional(Root);
685
+ },
686
+ });
687
+ const Root: any = z.object({ tag: z.string() }).merge(z.object({}).catchall(Cyclic));
688
+
689
+ const input: any = { tag: "root" };
690
+ input.child = { id: "1", next: input };
691
+
692
+ const out: any = Root.parse(input);
693
+ expect(out.child.next).toBe(out);
694
+ });
695
+
696
+ // the per-type switch can only enumerate kinds Zod ships, so an unknown `def.type` falls back to scanning the def; without it a cycle through a third-party container overflows the stack
697
+ test("detects a cycle through a user-defined container type", () => {
698
+ const MyBox: any = z.core.$constructor("MyBox", (inst: any, def: any) => {
699
+ z.core.$ZodType.init(inst, def);
700
+ inst._zod.parse = (payload: any, ctx: any) => {
701
+ if (payload.value === null || typeof payload.value !== "object") return payload;
702
+ const inner = def.inner._zod.run({ value: payload.value.v, issues: [] }, ctx);
703
+ payload.value = { v: inner.value };
704
+ return payload;
705
+ };
706
+ });
707
+
708
+ const Node: any = z.object({
709
+ id: z.string(),
710
+ get boxed() {
711
+ return z.optional(new MyBox({ type: "mybox", inner: z.lazy(() => Node) }));
712
+ },
713
+ });
714
+
715
+ const input: any = { id: "1" };
716
+ input.boxed = { v: input };
717
+
718
+ const out: any = Node.parse(input);
719
+ expect(out.boxed.v).toBe(out);
720
+ });
@@ -407,3 +407,49 @@ test("direction-aware defaults", () => {
407
407
  }
408
408
  `);
409
409
  });
410
+
411
+ test("default factory runs once per parse inside a container", () => {
412
+ let calls = 0;
413
+ const schema = z.object({
414
+ a: z.string().default(() => {
415
+ calls++;
416
+ return "d";
417
+ }),
418
+ });
419
+
420
+ expect(schema.parse({})).toEqual({ a: "d" });
421
+ expect(calls).toBe(1);
422
+
423
+ // the key is present, so the factory has nothing to produce
424
+ expect(schema.parse({ a: "given" })).toEqual({ a: "given" });
425
+ expect(calls).toBe(1);
426
+ });
427
+
428
+ test("prefault factory runs once per parse inside a container", () => {
429
+ let calls = 0;
430
+ const schema = z.object({
431
+ a: z.string().prefault(() => {
432
+ calls++;
433
+ return "d";
434
+ }),
435
+ });
436
+
437
+ expect(schema.parse({})).toEqual({ a: "d" });
438
+ expect(calls).toBe(1);
439
+ });
440
+
441
+ test("default factory does not run at compile time", () => {
442
+ let calls = 0;
443
+ const compiled = z.compile(
444
+ z.object({
445
+ a: z.string().default(() => {
446
+ calls++;
447
+ return "d";
448
+ }),
449
+ })
450
+ );
451
+
452
+ expect(calls).toBe(0);
453
+ expect(compiled.parse({})).toEqual({ a: "d" });
454
+ expect(calls).toBe(1);
455
+ });
@@ -1,5 +1,5 @@
1
1
  import type * as errors from "./errors.js";
2
- import type { $ZodMemoizer, $ZodType, ParseContextInternal, ParsePayload } from "./schemas.js";
2
+ import type { $ZodMemoizer, $ZodType, $ZodTypeDef, ParseContextInternal, ParsePayload } from "./schemas.js";
3
3
  import type * as util from "./util.js";
4
4
 
5
5
  export class $ZodCyclicError extends Error {
@@ -48,18 +48,94 @@ function isRecursive(inst: $ZodType, stack: Set<object>): boolean {
48
48
  };
49
49
 
50
50
  const def = inst._zod.def as any;
51
- if (def.type === "lazy") {
52
- check((inst as any)._zod.innerType);
53
- } else {
54
- // $ZodObject redefines `shape` as a non-enumerable accessor, so `for...in` misses it.
55
- const shape = def.shape;
56
- // `for...in` skips symbols, so a cycle through a declared symbol key would read as non-recursive
57
- if (shape) for (const key of Reflect.ownKeys(shape)) check(shape[key]);
58
- for (const key in def) {
59
- const value = def[key];
60
- if (!value || typeof value !== "object") continue;
61
- if (value._zod) check(value);
62
- else if (Array.isArray(value)) for (const el of value) check(el);
51
+ const kind = def.type as $ZodTypeDef["type"];
52
+ switch (kind) {
53
+ case "object": {
54
+ // `Reflect.ownKeys` rather than `Object.keys`, so a cycle through a declared symbol key is still seen
55
+ for (const key of Reflect.ownKeys(def.shape)) check(def.shape[key]);
56
+ check(def.catchall);
57
+ break;
58
+ }
59
+ case "array":
60
+ check(def.element);
61
+ break;
62
+ case "tuple":
63
+ for (const el of def.items) check(el);
64
+ check(def.rest);
65
+ break;
66
+ case "record":
67
+ case "map":
68
+ check(def.keyType);
69
+ check(def.valueType);
70
+ break;
71
+ case "set":
72
+ check(def.valueType);
73
+ break;
74
+ case "union":
75
+ for (const el of def.options) check(el);
76
+ break;
77
+ case "intersection":
78
+ check(def.left);
79
+ check(def.right);
80
+ break;
81
+ case "optional":
82
+ case "nullable":
83
+ case "default":
84
+ case "prefault":
85
+ case "catch":
86
+ case "readonly":
87
+ case "nonoptional":
88
+ case "promise":
89
+ case "success":
90
+ check(def.innerType);
91
+ break;
92
+ case "pipe":
93
+ check(def.in);
94
+ check(def.out);
95
+ break;
96
+ case "function":
97
+ check(def.input);
98
+ check(def.output);
99
+ break;
100
+ // reading `_zod.innerType` resolves the getter once and caches it
101
+ case "lazy":
102
+ check((inst as any)._zod.innerType);
103
+ break;
104
+ // a leaf by choice: `parts` are regex fragments, not data positions
105
+ case "template_literal":
106
+ // leaves
107
+ case "string":
108
+ case "number":
109
+ case "int":
110
+ case "boolean":
111
+ case "bigint":
112
+ case "symbol":
113
+ case "undefined":
114
+ case "null":
115
+ case "void":
116
+ case "never":
117
+ case "any":
118
+ case "unknown":
119
+ case "date":
120
+ case "nan":
121
+ case "enum":
122
+ case "literal":
123
+ case "file":
124
+ case "transform":
125
+ case "custom":
126
+ break;
127
+ default: {
128
+ // a new built-in kind becomes a compile error here
129
+ kind satisfies never;
130
+ // a user-defined kind can still hold children, and only its author knows where, so fall back to scanning the def — skipping accessors, since reading one can run user code
131
+ for (const key in def) {
132
+ const desc = Object.getOwnPropertyDescriptor(def, key);
133
+ if (!desc || desc.get) continue;
134
+ const value = desc.value;
135
+ if (!value || typeof value !== "object") continue;
136
+ if (value._zod) check(value);
137
+ else if (Array.isArray(value)) for (const el of value) check(el);
138
+ }
63
139
  }
64
140
  }
65
141
 
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 5,
4
- patch: 3 as number,
4
+ patch: 4 as number,
5
5
  } as const;
@@ -34,25 +34,102 @@ function isRecursive(inst, stack) {
34
34
  result = true;
35
35
  };
36
36
  const def = inst._zod.def;
37
- if (def.type === "lazy") {
38
- check(inst._zod.innerType);
39
- }
40
- else {
41
- // $ZodObject redefines `shape` as a non-enumerable accessor, so `for...in` misses it.
42
- const shape = def.shape;
43
- // `for...in` skips symbols, so a cycle through a declared symbol key would read as non-recursive
44
- if (shape)
45
- for (const key of Reflect.ownKeys(shape))
46
- check(shape[key]);
47
- for (const key in def) {
48
- const value = def[key];
49
- if (!value || typeof value !== "object")
50
- continue;
51
- if (value._zod)
52
- check(value);
53
- else if (Array.isArray(value))
54
- for (const el of value)
55
- check(el);
37
+ const kind = def.type;
38
+ switch (kind) {
39
+ case "object": {
40
+ // `Reflect.ownKeys` rather than `Object.keys`, so a cycle through a declared symbol key is still seen
41
+ for (const key of Reflect.ownKeys(def.shape))
42
+ check(def.shape[key]);
43
+ check(def.catchall);
44
+ break;
45
+ }
46
+ case "array":
47
+ check(def.element);
48
+ break;
49
+ case "tuple":
50
+ for (const el of def.items)
51
+ check(el);
52
+ check(def.rest);
53
+ break;
54
+ case "record":
55
+ case "map":
56
+ check(def.keyType);
57
+ check(def.valueType);
58
+ break;
59
+ case "set":
60
+ check(def.valueType);
61
+ break;
62
+ case "union":
63
+ for (const el of def.options)
64
+ check(el);
65
+ break;
66
+ case "intersection":
67
+ check(def.left);
68
+ check(def.right);
69
+ break;
70
+ case "optional":
71
+ case "nullable":
72
+ case "default":
73
+ case "prefault":
74
+ case "catch":
75
+ case "readonly":
76
+ case "nonoptional":
77
+ case "promise":
78
+ case "success":
79
+ check(def.innerType);
80
+ break;
81
+ case "pipe":
82
+ check(def.in);
83
+ check(def.out);
84
+ break;
85
+ case "function":
86
+ check(def.input);
87
+ check(def.output);
88
+ break;
89
+ // reading `_zod.innerType` resolves the getter once and caches it
90
+ case "lazy":
91
+ check(inst._zod.innerType);
92
+ break;
93
+ // a leaf by choice: `parts` are regex fragments, not data positions
94
+ case "template_literal":
95
+ // leaves
96
+ case "string":
97
+ case "number":
98
+ case "int":
99
+ case "boolean":
100
+ case "bigint":
101
+ case "symbol":
102
+ case "undefined":
103
+ case "null":
104
+ case "void":
105
+ case "never":
106
+ case "any":
107
+ case "unknown":
108
+ case "date":
109
+ case "nan":
110
+ case "enum":
111
+ case "literal":
112
+ case "file":
113
+ case "transform":
114
+ case "custom":
115
+ break;
116
+ default: {
117
+ // a new built-in kind becomes a compile error here
118
+ kind;
119
+ // a user-defined kind can still hold children, and only its author knows where, so fall back to scanning the def — skipping accessors, since reading one can run user code
120
+ for (const key in def) {
121
+ const desc = Object.getOwnPropertyDescriptor(def, key);
122
+ if (!desc || desc.get)
123
+ continue;
124
+ const value = desc.value;
125
+ if (!value || typeof value !== "object")
126
+ continue;
127
+ if (value._zod)
128
+ check(value);
129
+ else if (Array.isArray(value))
130
+ for (const el of value)
131
+ check(el);
132
+ }
56
133
  }
57
134
  }
58
135
  stack.delete(inst);
@@ -27,25 +27,102 @@ function isRecursive(inst, stack) {
27
27
  result = true;
28
28
  };
29
29
  const def = inst._zod.def;
30
- if (def.type === "lazy") {
31
- check(inst._zod.innerType);
32
- }
33
- else {
34
- // $ZodObject redefines `shape` as a non-enumerable accessor, so `for...in` misses it.
35
- const shape = def.shape;
36
- // `for...in` skips symbols, so a cycle through a declared symbol key would read as non-recursive
37
- if (shape)
38
- for (const key of Reflect.ownKeys(shape))
39
- check(shape[key]);
40
- for (const key in def) {
41
- const value = def[key];
42
- if (!value || typeof value !== "object")
43
- continue;
44
- if (value._zod)
45
- check(value);
46
- else if (Array.isArray(value))
47
- for (const el of value)
48
- check(el);
30
+ const kind = def.type;
31
+ switch (kind) {
32
+ case "object": {
33
+ // `Reflect.ownKeys` rather than `Object.keys`, so a cycle through a declared symbol key is still seen
34
+ for (const key of Reflect.ownKeys(def.shape))
35
+ check(def.shape[key]);
36
+ check(def.catchall);
37
+ break;
38
+ }
39
+ case "array":
40
+ check(def.element);
41
+ break;
42
+ case "tuple":
43
+ for (const el of def.items)
44
+ check(el);
45
+ check(def.rest);
46
+ break;
47
+ case "record":
48
+ case "map":
49
+ check(def.keyType);
50
+ check(def.valueType);
51
+ break;
52
+ case "set":
53
+ check(def.valueType);
54
+ break;
55
+ case "union":
56
+ for (const el of def.options)
57
+ check(el);
58
+ break;
59
+ case "intersection":
60
+ check(def.left);
61
+ check(def.right);
62
+ break;
63
+ case "optional":
64
+ case "nullable":
65
+ case "default":
66
+ case "prefault":
67
+ case "catch":
68
+ case "readonly":
69
+ case "nonoptional":
70
+ case "promise":
71
+ case "success":
72
+ check(def.innerType);
73
+ break;
74
+ case "pipe":
75
+ check(def.in);
76
+ check(def.out);
77
+ break;
78
+ case "function":
79
+ check(def.input);
80
+ check(def.output);
81
+ break;
82
+ // reading `_zod.innerType` resolves the getter once and caches it
83
+ case "lazy":
84
+ check(inst._zod.innerType);
85
+ break;
86
+ // a leaf by choice: `parts` are regex fragments, not data positions
87
+ case "template_literal":
88
+ // leaves
89
+ case "string":
90
+ case "number":
91
+ case "int":
92
+ case "boolean":
93
+ case "bigint":
94
+ case "symbol":
95
+ case "undefined":
96
+ case "null":
97
+ case "void":
98
+ case "never":
99
+ case "any":
100
+ case "unknown":
101
+ case "date":
102
+ case "nan":
103
+ case "enum":
104
+ case "literal":
105
+ case "file":
106
+ case "transform":
107
+ case "custom":
108
+ break;
109
+ default: {
110
+ // a new built-in kind becomes a compile error here
111
+ kind;
112
+ // a user-defined kind can still hold children, and only its author knows where, so fall back to scanning the def — skipping accessors, since reading one can run user code
113
+ for (const key in def) {
114
+ const desc = Object.getOwnPropertyDescriptor(def, key);
115
+ if (!desc || desc.get)
116
+ continue;
117
+ const value = desc.value;
118
+ if (!value || typeof value !== "object")
119
+ continue;
120
+ if (value._zod)
121
+ check(value);
122
+ else if (Array.isArray(value))
123
+ for (const el of value)
124
+ check(el);
125
+ }
49
126
  }
50
127
  }
51
128
  stack.delete(inst);
@@ -4,5 +4,5 @@ exports.version = void 0;
4
4
  exports.version = {
5
5
  major: 4,
6
6
  minor: 5,
7
- patch: 3,
7
+ patch: 4,
8
8
  };
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 5,
4
- patch: 3,
4
+ patch: 4,
5
5
  };