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 +1 -1
- package/src/v4/classic/tests/cyclic-data.test.ts +42 -0
- package/src/v4/classic/tests/default.test.ts +46 -0
- package/src/v4/core/memoizer.ts +89 -13
- package/src/v4/core/versions.ts +1 -1
- package/v4/core/memoizer.cjs +96 -19
- package/v4/core/memoizer.js +96 -19
- package/v4/core/versions.cjs +1 -1
- package/v4/core/versions.js +1 -1
package/package.json
CHANGED
|
@@ -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
|
+
});
|
package/src/v4/core/memoizer.ts
CHANGED
|
@@ -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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
package/src/v4/core/versions.ts
CHANGED
package/v4/core/memoizer.cjs
CHANGED
|
@@ -34,25 +34,102 @@ function isRecursive(inst, stack) {
|
|
|
34
34
|
result = true;
|
|
35
35
|
};
|
|
36
36
|
const def = inst._zod.def;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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);
|
package/v4/core/memoizer.js
CHANGED
|
@@ -27,25 +27,102 @@ function isRecursive(inst, stack) {
|
|
|
27
27
|
result = true;
|
|
28
28
|
};
|
|
29
29
|
const def = inst._zod.def;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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);
|
package/v4/core/versions.cjs
CHANGED
package/v4/core/versions.js
CHANGED