zod 4.5.4 → 4.6.1
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/compile.cjs +19 -0
- package/index.cjs +19 -0
- package/locales/index.cjs +19 -0
- package/mini/index.cjs +19 -0
- package/package.json +8 -7
- package/src/v3/helpers/errorUtil.ts +1 -1
- package/src/v4/classic/checks.ts +0 -1
- package/src/v4/classic/external.ts +2 -0
- package/src/v4/classic/from-json-schema.ts +248 -25
- package/src/v4/classic/schemas.ts +255 -164
- package/src/v4/classic/tests/assignability.test.ts +59 -9
- package/src/v4/classic/tests/bigint.test.ts +6 -0
- package/src/v4/classic/tests/continuability.test.ts +2 -2
- package/src/v4/classic/tests/cyclic-data.test.ts +274 -0
- package/src/v4/classic/tests/discriminated-unions.test.ts +112 -6
- package/src/v4/classic/tests/enum.test.ts +9 -0
- package/src/v4/classic/tests/error.test.ts +130 -1
- package/src/v4/classic/tests/firstparty.test.ts +4 -0
- package/src/v4/classic/tests/from-json-schema.test.ts +157 -0
- package/src/v4/classic/tests/iban.test.ts +81 -0
- package/src/v4/classic/tests/instance-footprint.test.ts +18 -2
- package/src/v4/classic/tests/instanceof.test.ts +27 -7
- package/src/v4/classic/tests/issue-schema.test.ts +88 -56
- package/src/v4/classic/tests/object.test.ts +56 -0
- package/src/v4/classic/tests/optin-ladder.test.ts +3 -2
- package/src/v4/classic/tests/properties.test.ts +225 -0
- package/src/v4/classic/tests/recursive-types.test.ts +46 -1
- package/src/v4/classic/tests/standard-schema.test.ts +19 -0
- package/src/v4/classic/tests/string.test.ts +25 -0
- package/src/v4/classic/tests/template-literal.test.ts +33 -1
- package/src/v4/classic/tests/to-json-schema.test.ts +147 -3
- package/src/v4/classic/tests/validate.test.ts +569 -0
- package/src/v4/core/api.ts +30 -6
- package/src/v4/core/checks.ts +1 -101
- package/src/v4/core/compile.ts +194 -82
- package/src/v4/core/doc.ts +6 -2
- package/src/v4/core/errors.ts +3 -6
- package/src/v4/core/json-schema-generator.ts +2 -2
- package/src/v4/core/json-schema-processors.ts +226 -68
- package/src/v4/core/memoizer.ts +78 -28
- package/src/v4/core/parse.ts +37 -16
- package/src/v4/core/regexes.ts +10 -4
- package/src/v4/core/schemas.ts +350 -113
- package/src/v4/core/tests/cached.test.ts +54 -0
- package/src/v4/core/tests/compile-generative.test.ts +445 -0
- package/src/v4/core/tests/compile.test.ts +222 -2
- package/src/v4/core/tests/email-regex.test.ts +140 -0
- package/src/v4/core/tests/locales/tg.test.ts +85 -0
- package/src/v4/core/tests/polyfill-collision.test.ts +46 -0
- package/src/v4/core/to-json-schema.ts +10 -8
- package/src/v4/core/util.ts +222 -147
- package/src/v4/core/versions.ts +2 -2
- package/src/v4/core/visit.ts +11 -0
- package/src/v4/locales/ar.ts +1 -0
- package/src/v4/locales/az.ts +1 -0
- package/src/v4/locales/be.ts +1 -0
- package/src/v4/locales/bg.ts +1 -0
- package/src/v4/locales/bn.ts +1 -0
- package/src/v4/locales/ca.ts +1 -0
- package/src/v4/locales/ckb.ts +1 -0
- package/src/v4/locales/cs.ts +1 -0
- package/src/v4/locales/da.ts +1 -0
- package/src/v4/locales/de.ts +1 -0
- package/src/v4/locales/el.ts +1 -0
- package/src/v4/locales/en.ts +1 -0
- package/src/v4/locales/eo.ts +1 -0
- package/src/v4/locales/es.ts +1 -0
- package/src/v4/locales/fa.ts +1 -0
- package/src/v4/locales/fi.ts +1 -0
- package/src/v4/locales/fr-CA.ts +1 -0
- package/src/v4/locales/fr.ts +1 -0
- package/src/v4/locales/gu.ts +1 -0
- package/src/v4/locales/he.ts +1 -0
- package/src/v4/locales/hi.ts +1 -0
- package/src/v4/locales/hr.ts +1 -0
- package/src/v4/locales/hu.ts +1 -0
- package/src/v4/locales/hy.ts +1 -0
- package/src/v4/locales/id.ts +1 -0
- package/src/v4/locales/index.ts +1 -0
- package/src/v4/locales/is.ts +1 -0
- package/src/v4/locales/it.ts +1 -0
- package/src/v4/locales/ja.ts +1 -0
- package/src/v4/locales/ka.ts +1 -0
- package/src/v4/locales/km.ts +1 -0
- package/src/v4/locales/kn.ts +1 -0
- package/src/v4/locales/ko.ts +1 -0
- package/src/v4/locales/lt.ts +1 -0
- package/src/v4/locales/mk.ts +1 -0
- package/src/v4/locales/ms.ts +1 -0
- package/src/v4/locales/ne.ts +1 -0
- package/src/v4/locales/nl.ts +1 -0
- package/src/v4/locales/nn.ts +1 -0
- package/src/v4/locales/no.ts +1 -0
- package/src/v4/locales/ota.ts +1 -0
- package/src/v4/locales/pl.ts +1 -0
- package/src/v4/locales/ps.ts +1 -0
- package/src/v4/locales/pt-BR.ts +1 -0
- package/src/v4/locales/pt.ts +1 -0
- package/src/v4/locales/ro.ts +1 -0
- package/src/v4/locales/ru.ts +1 -0
- package/src/v4/locales/sk.ts +1 -0
- package/src/v4/locales/sl.ts +1 -0
- package/src/v4/locales/sv.ts +1 -0
- package/src/v4/locales/ta.ts +1 -0
- package/src/v4/locales/tg.ts +134 -0
- package/src/v4/locales/th.ts +1 -0
- package/src/v4/locales/tk.ts +1 -0
- package/src/v4/locales/tr.ts +1 -0
- package/src/v4/locales/uk.ts +1 -0
- package/src/v4/locales/ur.ts +1 -0
- package/src/v4/locales/uz.ts +1 -0
- package/src/v4/locales/vi.ts +1 -0
- package/src/v4/locales/yo.ts +1 -0
- package/src/v4/locales/zh-CN.ts +1 -0
- package/src/v4/locales/zh-TW.ts +1 -0
- package/src/v4/mini/checks.ts +0 -1
- package/src/v4/mini/external.ts +2 -0
- package/src/v4/mini/schemas.ts +38 -2
- package/src/v4/mini/tests/checks.test.ts +1 -1
- package/src/v4/mini/tests/computed.test.ts +11 -17
- package/src/v4/mini/tests/index.test.ts +8 -0
- package/src/v4/mini/tests/recursive-types.test.ts +16 -0
- package/src/v4/mini/tests/string.test.ts +16 -0
- package/v3/ZodError.cjs +19 -0
- package/v3/errors.cjs +19 -0
- package/v3/external.cjs +19 -0
- package/v3/helpers/enumUtil.cjs +19 -0
- package/v3/helpers/errorUtil.cjs +20 -1
- package/v3/helpers/errorUtil.js +1 -1
- package/v3/helpers/parseUtil.cjs +19 -0
- package/v3/helpers/partialUtil.cjs +19 -0
- package/v3/helpers/typeAliases.cjs +19 -0
- package/v3/helpers/util.cjs +19 -0
- package/v3/index.cjs +19 -0
- package/v3/standard-schema.cjs +19 -0
- package/v3/types.cjs +19 -0
- package/v4/classic/checks.cjs +20 -2
- package/v4/classic/checks.d.cts +1 -1
- package/v4/classic/checks.d.ts +1 -1
- package/v4/classic/checks.js +1 -1
- package/v4/classic/coerce.cjs +19 -0
- package/v4/classic/compat.cjs +19 -0
- package/v4/classic/deep-partial.cjs +19 -0
- package/v4/classic/errors.cjs +19 -0
- package/v4/classic/external.cjs +22 -1
- package/v4/classic/external.d.cts +1 -1
- package/v4/classic/external.d.ts +1 -1
- package/v4/classic/external.js +1 -1
- package/v4/classic/from-json-schema.cjs +264 -24
- package/v4/classic/from-json-schema.js +245 -24
- package/v4/classic/in-out.cjs +19 -0
- package/v4/classic/index.cjs +19 -0
- package/v4/classic/iso.cjs +19 -0
- package/v4/classic/parse.cjs +19 -0
- package/v4/classic/schemas.cjs +100 -31
- package/v4/classic/schemas.d.cts +17 -1
- package/v4/classic/schemas.d.ts +17 -1
- package/v4/classic/schemas.js +77 -29
- package/v4/core/api.cjs +37 -2
- package/v4/core/api.d.cts +6 -4
- package/v4/core/api.d.ts +6 -4
- package/v4/core/api.js +17 -2
- package/v4/core/checks.cjs +19 -96
- package/v4/core/checks.d.cts +1 -1
- package/v4/core/checks.d.ts +1 -1
- package/v4/core/checks.js +0 -96
- package/v4/core/compile.cjs +165 -66
- package/v4/core/compile.d.cts +18 -4
- package/v4/core/compile.d.ts +18 -4
- package/v4/core/compile.js +146 -67
- package/v4/core/core.cjs +19 -0
- package/v4/core/doc.cjs +26 -2
- package/v4/core/doc.js +7 -2
- package/v4/core/errors.cjs +21 -5
- package/v4/core/errors.js +2 -5
- package/v4/core/index.cjs +19 -0
- package/v4/core/json-schema-generator.cjs +20 -1
- package/v4/core/json-schema-generator.js +2 -2
- package/v4/core/json-schema-processors.cjs +215 -63
- package/v4/core/json-schema-processors.d.cts +17 -0
- package/v4/core/json-schema-processors.d.ts +17 -0
- package/v4/core/json-schema-processors.js +195 -64
- package/v4/core/json-schema.cjs +19 -0
- package/v4/core/memoizer.cjs +114 -23
- package/v4/core/memoizer.js +72 -23
- package/v4/core/parse.cjs +56 -16
- package/v4/core/parse.js +37 -16
- package/v4/core/regexes.cjs +29 -6
- package/v4/core/regexes.d.cts +2 -0
- package/v4/core/regexes.d.ts +2 -0
- package/v4/core/regexes.js +8 -4
- package/v4/core/registries.cjs +19 -0
- package/v4/core/schemas.cjs +322 -95
- package/v4/core/schemas.d.cts +42 -7
- package/v4/core/schemas.d.ts +42 -7
- package/v4/core/schemas.js +301 -94
- package/v4/core/standard-schema.cjs +19 -0
- package/v4/core/to-json-schema.cjs +29 -7
- package/v4/core/to-json-schema.d.cts +7 -3
- package/v4/core/to-json-schema.d.ts +7 -3
- package/v4/core/to-json-schema.js +8 -7
- package/v4/core/util.cjs +181 -143
- package/v4/core/util.d.cts +25 -4
- package/v4/core/util.d.ts +25 -4
- package/v4/core/util.js +160 -143
- package/v4/core/versions.cjs +21 -2
- package/v4/core/versions.d.cts +1 -1
- package/v4/core/versions.d.ts +1 -1
- package/v4/core/versions.js +2 -2
- package/v4/core/visit.cjs +31 -0
- package/v4/core/visit.js +12 -0
- package/v4/index.cjs +19 -0
- package/v4/locales/ar.cjs +1 -0
- package/v4/locales/ar.js +1 -0
- package/v4/locales/az.cjs +1 -0
- package/v4/locales/az.js +1 -0
- package/v4/locales/be.cjs +1 -0
- package/v4/locales/be.js +1 -0
- package/v4/locales/bg.cjs +1 -0
- package/v4/locales/bg.js +1 -0
- package/v4/locales/bn.cjs +1 -0
- package/v4/locales/bn.js +1 -0
- package/v4/locales/ca.cjs +1 -0
- package/v4/locales/ca.js +1 -0
- package/v4/locales/ckb.cjs +1 -0
- package/v4/locales/ckb.js +1 -0
- package/v4/locales/cs.cjs +1 -0
- package/v4/locales/cs.js +1 -0
- package/v4/locales/da.cjs +1 -0
- package/v4/locales/da.js +1 -0
- package/v4/locales/de.cjs +1 -0
- package/v4/locales/de.js +1 -0
- package/v4/locales/el.cjs +1 -0
- package/v4/locales/el.js +1 -0
- package/v4/locales/en.cjs +1 -0
- package/v4/locales/en.js +1 -0
- package/v4/locales/eo.cjs +1 -0
- package/v4/locales/eo.js +1 -0
- package/v4/locales/es.cjs +1 -0
- package/v4/locales/es.js +1 -0
- package/v4/locales/fa.cjs +1 -0
- package/v4/locales/fa.js +1 -0
- package/v4/locales/fi.cjs +1 -0
- package/v4/locales/fi.js +1 -0
- package/v4/locales/fr-CA.cjs +1 -0
- package/v4/locales/fr-CA.js +1 -0
- package/v4/locales/fr.cjs +1 -0
- package/v4/locales/fr.js +1 -0
- package/v4/locales/gu.cjs +1 -0
- package/v4/locales/gu.js +1 -0
- package/v4/locales/he.cjs +1 -0
- package/v4/locales/he.js +1 -0
- package/v4/locales/hi.cjs +1 -0
- package/v4/locales/hi.js +1 -0
- package/v4/locales/hr.cjs +1 -0
- package/v4/locales/hr.js +1 -0
- package/v4/locales/hu.cjs +1 -0
- package/v4/locales/hu.js +1 -0
- package/v4/locales/hy.cjs +1 -0
- package/v4/locales/hy.js +1 -0
- package/v4/locales/id.cjs +1 -0
- package/v4/locales/id.js +1 -0
- package/v4/locales/index.cjs +22 -1
- package/v4/locales/index.d.cts +1 -0
- package/v4/locales/index.d.ts +1 -0
- package/v4/locales/index.js +1 -0
- package/v4/locales/is.cjs +1 -0
- package/v4/locales/is.js +1 -0
- package/v4/locales/it.cjs +1 -0
- package/v4/locales/it.js +1 -0
- package/v4/locales/ja.cjs +1 -0
- package/v4/locales/ja.js +1 -0
- package/v4/locales/ka.cjs +1 -0
- package/v4/locales/ka.js +1 -0
- package/v4/locales/km.cjs +1 -0
- package/v4/locales/km.js +1 -0
- package/v4/locales/kn.cjs +1 -0
- package/v4/locales/kn.js +1 -0
- package/v4/locales/ko.cjs +1 -0
- package/v4/locales/ko.js +1 -0
- package/v4/locales/lt.cjs +1 -0
- package/v4/locales/lt.js +1 -0
- package/v4/locales/mk.cjs +1 -0
- package/v4/locales/mk.js +1 -0
- package/v4/locales/ms.cjs +1 -0
- package/v4/locales/ms.js +1 -0
- package/v4/locales/ne.cjs +1 -0
- package/v4/locales/ne.js +1 -0
- package/v4/locales/nl.cjs +1 -0
- package/v4/locales/nl.js +1 -0
- package/v4/locales/nn.cjs +1 -0
- package/v4/locales/nn.js +1 -0
- package/v4/locales/no.cjs +1 -0
- package/v4/locales/no.js +1 -0
- package/v4/locales/ota.cjs +1 -0
- package/v4/locales/ota.js +1 -0
- package/v4/locales/pl.cjs +1 -0
- package/v4/locales/pl.js +1 -0
- package/v4/locales/ps.cjs +1 -0
- package/v4/locales/ps.js +1 -0
- package/v4/locales/pt-BR.cjs +1 -0
- package/v4/locales/pt-BR.js +1 -0
- package/v4/locales/pt.cjs +1 -0
- package/v4/locales/pt.js +1 -0
- package/v4/locales/ro.cjs +1 -0
- package/v4/locales/ro.js +1 -0
- package/v4/locales/ru.cjs +1 -0
- package/v4/locales/ru.js +1 -0
- package/v4/locales/sk.cjs +1 -0
- package/v4/locales/sk.js +1 -0
- package/v4/locales/sl.cjs +1 -0
- package/v4/locales/sl.js +1 -0
- package/v4/locales/sv.cjs +1 -0
- package/v4/locales/sv.js +1 -0
- package/v4/locales/ta.cjs +1 -0
- package/v4/locales/ta.js +1 -0
- package/v4/locales/tg.cjs +143 -0
- package/v4/locales/tg.d.cts +5 -0
- package/v4/locales/tg.d.ts +4 -0
- package/v4/locales/tg.js +116 -0
- package/v4/locales/th.cjs +1 -0
- package/v4/locales/th.js +1 -0
- package/v4/locales/tk.cjs +1 -0
- package/v4/locales/tk.js +1 -0
- package/v4/locales/tr.cjs +1 -0
- package/v4/locales/tr.js +1 -0
- package/v4/locales/uk.cjs +1 -0
- package/v4/locales/uk.js +1 -0
- package/v4/locales/ur.cjs +1 -0
- package/v4/locales/ur.js +1 -0
- package/v4/locales/uz.cjs +1 -0
- package/v4/locales/uz.js +1 -0
- package/v4/locales/vi.cjs +1 -0
- package/v4/locales/vi.js +1 -0
- package/v4/locales/yo.cjs +1 -0
- package/v4/locales/yo.js +1 -0
- package/v4/locales/zh-CN.cjs +1 -0
- package/v4/locales/zh-CN.js +1 -0
- package/v4/locales/zh-TW.cjs +1 -0
- package/v4/locales/zh-TW.js +1 -0
- package/v4/mini/checks.cjs +20 -2
- package/v4/mini/checks.d.cts +1 -1
- package/v4/mini/checks.d.ts +1 -1
- package/v4/mini/checks.js +1 -1
- package/v4/mini/coerce.cjs +19 -0
- package/v4/mini/deep-partial.cjs +19 -0
- package/v4/mini/external.cjs +22 -1
- package/v4/mini/external.d.cts +1 -1
- package/v4/mini/external.d.ts +1 -1
- package/v4/mini/external.js +1 -1
- package/v4/mini/in-out.cjs +19 -0
- package/v4/mini/index.cjs +19 -0
- package/v4/mini/iso.cjs +19 -0
- package/v4/mini/parse.cjs +19 -0
- package/v4/mini/schemas.cjs +42 -4
- package/v4/mini/schemas.d.cts +8 -0
- package/v4/mini/schemas.d.ts +8 -0
- package/v4/mini/schemas.js +19 -2
- package/v4-mini/index.cjs +19 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import v8 from "node:v8";
|
|
2
|
+
import vm from "node:vm";
|
|
1
3
|
import { expect, test } from "vitest";
|
|
2
4
|
import * as z from "zod/v4";
|
|
3
5
|
|
|
@@ -718,3 +720,275 @@ test("detects a cycle through a user-defined container type", () => {
|
|
|
718
720
|
const out: any = Node.parse(input);
|
|
719
721
|
expect(out.boxed.v).toBe(out);
|
|
720
722
|
});
|
|
723
|
+
|
|
724
|
+
// #6526: a factory returns fresh instances, so the identity walk never revisits a node; it descends until the stack overflows unless it stops at the deferred edge
|
|
725
|
+
test("parses a factory-built recursive schema", () => {
|
|
726
|
+
const Node = (): any =>
|
|
727
|
+
z.object({
|
|
728
|
+
id: z.number(),
|
|
729
|
+
get kids() {
|
|
730
|
+
return z.array(Node());
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
const S = z.object({ root: Node() });
|
|
735
|
+
const out = S.parse({ root: { id: 1, kids: [{ id: 2, kids: [] }] } });
|
|
736
|
+
expect(out.root.kids[0].id).toBe(2);
|
|
737
|
+
expect(S.safeParse({ root: { id: 1, kids: [{ id: "x", kids: [] }] } }).success).toBe(false);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("parses a factory-built recursive discriminated union at the root and nested", () => {
|
|
741
|
+
const Geometry = (): any =>
|
|
742
|
+
z.discriminatedUnion("type", [
|
|
743
|
+
z.object({ type: z.literal("Point"), coordinates: z.array(z.number()) }),
|
|
744
|
+
z.looseObject({
|
|
745
|
+
type: z.literal("GeometryCollection"),
|
|
746
|
+
get geometries() {
|
|
747
|
+
return z.array(Geometry());
|
|
748
|
+
},
|
|
749
|
+
}),
|
|
750
|
+
]);
|
|
751
|
+
|
|
752
|
+
const collection = { type: "GeometryCollection", geometries: [{ type: "Point", coordinates: [1, 2] }] };
|
|
753
|
+
expect(Geometry().parse(collection).geometries[0].coordinates).toEqual([1, 2]);
|
|
754
|
+
expect(z.object({ g: Geometry() }).parse({ g: collection }).g.type).toBe("GeometryCollection");
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test("the walk reports a cycle for a deferred edge instead of resolving it", () => {
|
|
758
|
+
const Node = (): any =>
|
|
759
|
+
z.object({
|
|
760
|
+
get self() {
|
|
761
|
+
return Node();
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
expect(z.core.isRecursiveSchema(Node())).toBe(true);
|
|
765
|
+
|
|
766
|
+
// conservative by design: a getter that closes no cycle is reported too, and pays only the memoizer it keeps
|
|
767
|
+
const Leaf = z.object({ id: z.string() });
|
|
768
|
+
const forward: any = z.object({
|
|
769
|
+
get leaf() {
|
|
770
|
+
return Leaf;
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
expect(z.core.isRecursiveSchema(forward)).toBe(true);
|
|
774
|
+
|
|
775
|
+
// a lazy is followed one hop, which is what lets z.compile see past it; past that hop it is deferred like any other edge
|
|
776
|
+
expect(z.core.isRecursiveSchema(z.lazy(() => Leaf) as any)).toBe(false);
|
|
777
|
+
expect(z.core.isRecursiveSchema(z.lazy(() => z.lazy(() => Leaf)) as any)).toBe(true);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// the builders answer `shape` from an accessor that resolves the source's getters, so the walk has to reach the source schema itself
|
|
781
|
+
test("parses a factory-built recursive schema through every object builder", () => {
|
|
782
|
+
const wraps: [string, (base: () => any) => any][] = [
|
|
783
|
+
["extend", (b) => b().extend({ x: z.string() })],
|
|
784
|
+
["safeExtend", (b) => b().safeExtend({ x: z.string() })],
|
|
785
|
+
["merge", (b) => z.object({ x: z.string() }).merge(b())],
|
|
786
|
+
["partial", (b) => b().partial()],
|
|
787
|
+
["required", (b) => b().required()],
|
|
788
|
+
["pick", (b) => b().pick({ self: true })],
|
|
789
|
+
["omit", (b) => b().omit({ y: true })],
|
|
790
|
+
["chained", (b) => b().extend({ x: z.string() }).partial()],
|
|
791
|
+
// rebuilds the def without touching the shape, so the clone carries the source's accessor rather than a shape of its own
|
|
792
|
+
["strict", (b) => b().extend({ x: z.string() }).strict()],
|
|
793
|
+
["catchall", (b) => b().extend({ x: z.string() }).catchall(z.unknown())],
|
|
794
|
+
["meta", (b) => b().extend({ x: z.string() }).meta({ id: "node" })],
|
|
795
|
+
];
|
|
796
|
+
|
|
797
|
+
for (const [name, wrap] of wraps) {
|
|
798
|
+
const Node = (): any =>
|
|
799
|
+
wrap(() =>
|
|
800
|
+
z.object({
|
|
801
|
+
y: z.string(),
|
|
802
|
+
get self() {
|
|
803
|
+
return z.optional(Node());
|
|
804
|
+
},
|
|
805
|
+
})
|
|
806
|
+
);
|
|
807
|
+
const S = z.object({ r: Node() });
|
|
808
|
+
for (const attempt of ["first", "second"]) {
|
|
809
|
+
expect(() => S.safeParse({ r: { y: "s", x: "s" } }), `${name} (${attempt} parse)`).not.toThrow();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
test("an ordinary builder stays exact, so it still compiles", () => {
|
|
815
|
+
const plain = z.object({ a: z.string(), b: z.number() });
|
|
816
|
+
const derived: [string, any][] = [
|
|
817
|
+
["extend", plain.extend({ c: z.boolean() })],
|
|
818
|
+
["merge", plain.merge(z.object({ d: z.string() }))],
|
|
819
|
+
["partial", plain.partial()],
|
|
820
|
+
["required", plain.partial().required()],
|
|
821
|
+
["pick", plain.pick({ a: true })],
|
|
822
|
+
["omit", plain.omit({ a: true })],
|
|
823
|
+
["strict", plain.extend({ c: z.boolean() }).strict()],
|
|
824
|
+
["catchall", plain.extend({ c: z.boolean() }).catchall(z.unknown())],
|
|
825
|
+
["meta", plain.extend({ c: z.boolean() }).meta({ id: "plain" })],
|
|
826
|
+
];
|
|
827
|
+
for (const [name, schema] of derived) {
|
|
828
|
+
expect(z.core.isRecursiveSchema(schema), name).toBe(false);
|
|
829
|
+
expect(() => z.compile(schema, { strict: true }), name).not.toThrow();
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// a derived shape mirrors its source's descriptors, so dropping or overwriting the key that carries the cycle leaves nothing deferred and the walk is exact before anything parses
|
|
834
|
+
test("a builder that drops the recursive key ends up exact", () => {
|
|
835
|
+
const Node: any = z.object({
|
|
836
|
+
id: z.number(),
|
|
837
|
+
get self() {
|
|
838
|
+
return z.optional(Node);
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
const derived: [string, any, any][] = [
|
|
843
|
+
["omit", Node.omit({ self: true }), { id: 1 }],
|
|
844
|
+
["pick", Node.pick({ id: true }), { id: 1 }],
|
|
845
|
+
["extend overwrites it", Node.extend({ self: z.string() }), { id: 1, self: "x" }],
|
|
846
|
+
];
|
|
847
|
+
for (const [name, schema, input] of derived) {
|
|
848
|
+
expect(z.core.isRecursiveSchema(schema), name).toBe(false);
|
|
849
|
+
expect(() => z.compile(schema, { strict: true }), name).not.toThrow();
|
|
850
|
+
expect(schema.parse(input), name).toEqual(input);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// the branch that stays is still found
|
|
854
|
+
const kept = Node.extend({ x: z.string() });
|
|
855
|
+
expect(z.core.isRecursiveSchema(kept)).toBe(true);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// resolving a lazy runs user code, which can re-enter the walk; a walk that kept its certainty outside its own frames would have the inner one clobber the outer
|
|
859
|
+
test("a nested walk can't corrupt the one that resolved into it", () => {
|
|
860
|
+
const Node: any = z.object({
|
|
861
|
+
id: z.number(),
|
|
862
|
+
self: z.lazy(() => {
|
|
863
|
+
z.compile(z.string(), { strict: true });
|
|
864
|
+
return z.optional(Node);
|
|
865
|
+
}),
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
const omitted = Node.omit({ self: true });
|
|
869
|
+
// exact before anything parses: the mirrored shape kept only `id`, so nothing here is deferred
|
|
870
|
+
expect(z.core.isRecursiveSchema(omitted)).toBe(false);
|
|
871
|
+
omitted.parse({ id: 1 });
|
|
872
|
+
expect(z.core.isRecursiveSchema(omitted)).toBe(false);
|
|
873
|
+
expect(() => z.compile(omitted, { strict: true })).not.toThrow();
|
|
874
|
+
|
|
875
|
+
const shared = { id: 1 };
|
|
876
|
+
const out: any = z.object({ a: omitted, b: omitted }).parse({ a: shared, b: shared });
|
|
877
|
+
expect(out.a).not.toBe(out.b);
|
|
878
|
+
expect(z.core.isRecursiveSchema(Node)).toBe(true);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
test("detects a cycle that closes through a builder", () => {
|
|
882
|
+
const Node: any = z
|
|
883
|
+
.object({
|
|
884
|
+
id: z.number(),
|
|
885
|
+
get self() {
|
|
886
|
+
return z.optional(Node);
|
|
887
|
+
},
|
|
888
|
+
})
|
|
889
|
+
.extend({ x: z.string() });
|
|
890
|
+
|
|
891
|
+
const input: any = { id: 1, x: "s" };
|
|
892
|
+
input.self = input;
|
|
893
|
+
const out = Node.parse(input);
|
|
894
|
+
expect(out.self).toBe(out);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
test("a deferred edge stops being assumed a cycle once the graph resolves", () => {
|
|
898
|
+
const Leaf = z.object({ n: z.number() });
|
|
899
|
+
const Forward: any = z.object({
|
|
900
|
+
get a() {
|
|
901
|
+
return Leaf;
|
|
902
|
+
},
|
|
903
|
+
});
|
|
904
|
+
expect(z.core.isRecursiveSchema(Forward)).toBe(true);
|
|
905
|
+
Forward.parse({ a: { n: 1 } });
|
|
906
|
+
// the parse resolved the getter, so the walk can follow it and drop the assumption
|
|
907
|
+
expect(z.core.isRecursiveSchema(Forward)).toBe(false);
|
|
908
|
+
|
|
909
|
+
// a factory hands back a fresh subtree every time, so no parse ever resolves it into a finite graph
|
|
910
|
+
const Node = (): any =>
|
|
911
|
+
z.object({
|
|
912
|
+
get kids() {
|
|
913
|
+
return z.array(Node());
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
const Generated = Node();
|
|
917
|
+
expect(z.core.isRecursiveSchema(Generated)).toBe(true);
|
|
918
|
+
Generated.parse({ kids: [] });
|
|
919
|
+
expect(z.core.isRecursiveSchema(Generated)).toBe(true);
|
|
920
|
+
|
|
921
|
+
// a lazy that closes a real cycle stays recursive however often it resolves
|
|
922
|
+
const Cyclic: any = z.object({ id: z.number(), self: z.lazy(() => z.optional(Cyclic)) });
|
|
923
|
+
Cyclic.parse({ id: 1 });
|
|
924
|
+
expect(z.core.isRecursiveSchema(Cyclic)).toBe(true);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// `z.object` resolves its shape by spread, so a non-enumerable key is no part of it; `z.properties` reads every own key, so one there is still parsed and can still close a cycle
|
|
928
|
+
test("a non-enumerable key counts for z.properties and not for z.object", () => {
|
|
929
|
+
const props = (enumerable: boolean) => {
|
|
930
|
+
const shape: Record<string, any> = { id: z.number() };
|
|
931
|
+
const P = z.properties(shape);
|
|
932
|
+
Object.defineProperty(shape, "next", { value: z.lazy(() => z.optional(P)), enumerable, configurable: true });
|
|
933
|
+
return P;
|
|
934
|
+
};
|
|
935
|
+
expect(z.core.isRecursiveSchema(props(true) as any)).toBe(true);
|
|
936
|
+
expect(z.core.isRecursiveSchema(props(false) as any)).toBe(true);
|
|
937
|
+
|
|
938
|
+
const object = (enumerable: boolean) => {
|
|
939
|
+
const shape: Record<string, any> = { id: z.number() };
|
|
940
|
+
const O: any = z.object(shape);
|
|
941
|
+
Object.defineProperty(shape, "next", { value: z.lazy(() => z.optional(O)), enumerable, configurable: true });
|
|
942
|
+
return O;
|
|
943
|
+
};
|
|
944
|
+
expect(z.core.isRecursiveSchema(object(true))).toBe(true);
|
|
945
|
+
// the spread drops it, so nothing can ever parse it
|
|
946
|
+
expect(z.core.isRecursiveSchema(object(false))).toBe(false);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
test("the walk stays exact where nothing is deferred", () => {
|
|
950
|
+
let deep: any = z.string();
|
|
951
|
+
for (let i = 0; i < 300; i++) deep = z.object({ v: deep });
|
|
952
|
+
expect(z.core.isRecursiveSchema(deep)).toBe(false);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test("parses a factory-built recursive schema through z.lazy", () => {
|
|
956
|
+
const Node = (): any =>
|
|
957
|
+
z.object({
|
|
958
|
+
id: z.number(),
|
|
959
|
+
child: z.lazy(() => z.optional(Node())),
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
const out = z.object({ root: Node() }).parse({ root: { id: 1, child: { id: 2, child: undefined } } });
|
|
963
|
+
expect(out.root.child.id).toBe(2);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
test("a finished parse pins nothing on the schema", async () => {
|
|
967
|
+
// es2020 is the target, and its lib predates WeakRef
|
|
968
|
+
type Weak<T extends object> = { deref(): T | undefined };
|
|
969
|
+
const { WeakRef: Weak } = globalThis as unknown as { WeakRef: new <T extends object>(target: T) => Weak<T> };
|
|
970
|
+
// vitest carries no --expose-gc, so reach the collector the way node's own tests do
|
|
971
|
+
v8.setFlagsFromString("--expose-gc");
|
|
972
|
+
const gc = vm.runInNewContext("gc") as () => void;
|
|
973
|
+
|
|
974
|
+
const Node: any = z.object({
|
|
975
|
+
id: z.number(),
|
|
976
|
+
get next() {
|
|
977
|
+
return z.optional(Node);
|
|
978
|
+
},
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
const ref = ((): Weak<object> => {
|
|
982
|
+
const input = { id: 1, next: { id: 2, next: undefined } };
|
|
983
|
+
Node.parse(input);
|
|
984
|
+
return new Weak(input);
|
|
985
|
+
})();
|
|
986
|
+
|
|
987
|
+
for (let attempt = 0; attempt < 10 && ref.deref(); attempt++) {
|
|
988
|
+
// the input stays on the stack until a macrotask boundary
|
|
989
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
990
|
+
gc();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
expect(ref.deref()).toBeUndefined();
|
|
994
|
+
});
|
|
@@ -740,6 +740,26 @@ test("encode with codec discriminator", () => {
|
|
|
740
740
|
expect(encoded).toEqual({ type: 1, value: "hello" });
|
|
741
741
|
});
|
|
742
742
|
|
|
743
|
+
test("nested encoding does not select a sibling from forward discriminator values", async () => {
|
|
744
|
+
const tag = (value: "a" | "b") =>
|
|
745
|
+
z.codec(z.literal(value).default(value), z.undefined(), {
|
|
746
|
+
decode: () => undefined,
|
|
747
|
+
encode: () => value,
|
|
748
|
+
});
|
|
749
|
+
const inner = z.discriminatedUnion("type", [
|
|
750
|
+
z.object({ type: tag("a"), value: z.literal("a") }),
|
|
751
|
+
z.object({ type: tag("b"), value: z.literal("b") }),
|
|
752
|
+
]);
|
|
753
|
+
const outer = z.discriminatedUnion("type", [inner, z.object({ type: z.undefined(), value: z.string() })]);
|
|
754
|
+
for (const value of ["a", "b"] as const) {
|
|
755
|
+
const input = { type: undefined, value };
|
|
756
|
+
const expected = { type: value, value };
|
|
757
|
+
expect(z.encode(inner, input)).toEqual(expected);
|
|
758
|
+
expect(z.encode(outer, input)).toEqual(expected);
|
|
759
|
+
expect(await z.encodeAsync(outer, input)).toEqual(expected);
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
743
763
|
test("getDiscriminatedOption", () => {
|
|
744
764
|
const fruit = z.object({ type: z.literal("fruit"), seeds: z.boolean() });
|
|
745
765
|
const veg = z.object({ type: z.literal("vegetable"), leafy: z.boolean() });
|
|
@@ -811,9 +831,14 @@ test.each(["__proto__", "constructor", "toString", "hasOwnProperty", "valueOf"])
|
|
|
811
831
|
}
|
|
812
832
|
);
|
|
813
833
|
|
|
814
|
-
// An omittable discriminator reads back as undefined at the lookup, exactly as TypeScript sees it: `{ k?: "a" } | { k?: "c" }` does not narrow on `k === undefined`.
|
|
815
834
|
test("an omittable discriminator claims undefined", () => {
|
|
816
|
-
const omittable = [
|
|
835
|
+
const omittable = [
|
|
836
|
+
z.exactOptional(z.literal("a")),
|
|
837
|
+
z.optional(z.literal("a")),
|
|
838
|
+
z.literal("a").default("a"),
|
|
839
|
+
z.literal("a").prefault("a"),
|
|
840
|
+
z.literal("a").catch("a"),
|
|
841
|
+
];
|
|
817
842
|
for (const k of omittable) {
|
|
818
843
|
expect(z.object({ k })._zod.propValues.k).toEqual(new Set(["a", undefined]));
|
|
819
844
|
}
|
|
@@ -827,10 +852,91 @@ test("an omittable discriminator claims undefined", () => {
|
|
|
827
852
|
expect(z.union(options).safeParse({ x: "s" }).success).toEqual(true);
|
|
828
853
|
expect(z.discriminatedUnion("k", options).safeParse({ k: "b", y: 1 }).success).toEqual(true);
|
|
829
854
|
|
|
830
|
-
//
|
|
855
|
+
// ambiguous absence does not prevent explicit tags from routing
|
|
831
856
|
for (const k of omittable) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
).
|
|
857
|
+
const schema = z.discriminatedUnion("k", [z.object({ k }), z.object({ k: z.exactOptional(z.literal("c")) })]);
|
|
858
|
+
expect(schema.parse({ k: "a" })).toEqual({ k: "a" });
|
|
859
|
+
expect(schema.parse({ k: "c" })).toEqual({ k: "c" });
|
|
860
|
+
expect(schema.safeParse({}).success).toBe(false);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test("defaulted discriminators preserve tagged parsing without guessing a member", async () => {
|
|
865
|
+
const a = z.object({ type: z.literal("a").default("a"), x: z.number().positive() });
|
|
866
|
+
const b = z.object({ type: z.literal("b").default("b"), y: z.string() });
|
|
867
|
+
const c = z.object({ type: z.literal("c").default("c"), z: z.boolean() });
|
|
868
|
+
for (const options of [
|
|
869
|
+
[a, b, c],
|
|
870
|
+
[c, b, a],
|
|
871
|
+
] as const) {
|
|
872
|
+
const schema = z.discriminatedUnion("type", options);
|
|
873
|
+
for (const input of [a.parse({ x: 1 }), b.parse({ y: "s" }), c.parse({ z: true })]) {
|
|
874
|
+
expect(schema.parse(input)).toEqual(input);
|
|
875
|
+
expect((await schema.safeParseAsync(input)).success).toBe(true);
|
|
876
|
+
}
|
|
877
|
+
for (const input of [{ x: 1, y: "s", z: true }, { type: undefined }, { type: "other" }, { type: "a", x: -1 }]) {
|
|
878
|
+
expect(schema.safeParse(input).success).toBe(false);
|
|
879
|
+
expect((await schema.safeParseAsync(input)).success).toBe(false);
|
|
880
|
+
}
|
|
881
|
+
const result = schema.safeParse({});
|
|
882
|
+
expect(result.error?.issues[0]).toMatchObject({
|
|
883
|
+
code: "invalid_union",
|
|
884
|
+
path: ["type"],
|
|
885
|
+
options: options.map((o) => o.shape.type.unwrap().value),
|
|
886
|
+
});
|
|
887
|
+
expect(z.getDiscriminatedOption(schema, "a")).toBe(a);
|
|
888
|
+
}
|
|
889
|
+
const unique = z.discriminatedUnion("type", [a, b.safeExtend({ type: b.shape.type.unwrap() })]);
|
|
890
|
+
expect(unique.parse({ x: 1 })).toEqual({ type: "a", x: 1 });
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
test("undefined collisions are value-based and discriminator lookup rejects ambiguity", () => {
|
|
894
|
+
const a = z.object({ type: z.literal("a").optional() });
|
|
895
|
+
const absent = z.object({ type: z.undefined() });
|
|
896
|
+
for (const options of [
|
|
897
|
+
[a, absent],
|
|
898
|
+
[absent, a],
|
|
899
|
+
[absent, absent, a],
|
|
900
|
+
] as const) {
|
|
901
|
+
const schema = z.discriminatedUnion("type", options);
|
|
902
|
+
expect(schema.parse({ type: "a" })).toEqual({ type: "a" });
|
|
903
|
+
expect(schema.safeParse({}).success).toBe(false);
|
|
904
|
+
expect(() => z.getDiscriminatedOption(schema, undefined)).toThrow('Ambiguous discriminator value "undefined"');
|
|
905
|
+
}
|
|
906
|
+
const unique = z.discriminatedUnion("type", [absent, z.object({ type: z.literal("a") })]);
|
|
907
|
+
expect(unique.parse({ type: undefined })).toEqual({ type: undefined });
|
|
908
|
+
expect(z.getDiscriminatedOption(unique, undefined)).toBe(absent);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test("non-undefined discriminator collisions remain schema errors", () => {
|
|
912
|
+
for (const tag of [z.literal("a").default("a"), z.literal(["a", "b"]), z.literal("a").nullable()]) {
|
|
913
|
+
const schema = z.discriminatedUnion("type", [z.object({ type: tag }), z.object({ type: tag })]);
|
|
914
|
+
expect(() => schema.safeParse({ type: "a" })).toThrow(/Duplicate discriminator value/);
|
|
915
|
+
expect(() => z.encode(schema, { type: "a" })).toThrow(/Duplicate discriminator value/);
|
|
916
|
+
expect(() => z.getDiscriminatedOption(schema, "a")).toThrow(/Duplicate discriminator value/);
|
|
917
|
+
}
|
|
918
|
+
const nullable = z.discriminatedUnion("type", [
|
|
919
|
+
z.object({ type: z.literal("a").nullable() }),
|
|
920
|
+
z.object({ type: z.literal("b").nullable() }),
|
|
921
|
+
]);
|
|
922
|
+
expect(() => nullable.safeParse({ type: "a" })).toThrow('Duplicate discriminator value "null"');
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
test("nested defaulted unions only advertise routable discriminator values", () => {
|
|
926
|
+
const a = z.object({ type: z.literal("a").default("a"), group: z.literal("inner") });
|
|
927
|
+
const b = z.object({ type: z.literal("b").default("b"), group: z.literal("inner") });
|
|
928
|
+
const inner = z.discriminatedUnion("type", [a, b]);
|
|
929
|
+
const c = z.object({ type: z.literal("c").default("c"), group: z.literal("outer") });
|
|
930
|
+
expect(inner._zod.propValues.type).toEqual(new Set(["a", "b"]));
|
|
931
|
+
for (const schema of [
|
|
932
|
+
z.discriminatedUnion("type", [z.lazy(() => inner), c]),
|
|
933
|
+
z.discriminatedUnion("group", [inner, c]),
|
|
934
|
+
]) {
|
|
935
|
+
expect(schema.parse({ type: "a", group: "inner" })).toEqual({ type: "a", group: "inner" });
|
|
936
|
+
expect(schema.parse({ group: "outer" })).toEqual({ type: "c", group: "outer" });
|
|
937
|
+
expect(schema.safeParse({ group: "inner" }).success).toBe(false);
|
|
835
938
|
}
|
|
939
|
+
const fallback = z.discriminatedUnion("type", [a, b], { unionFallback: true });
|
|
940
|
+
expect(fallback._zod.propValues.type.has(undefined)).toBe(true);
|
|
941
|
+
expect(fallback.parse({ group: "inner" })).toEqual({ type: "a", group: "inner" });
|
|
836
942
|
});
|
|
@@ -87,6 +87,15 @@ test("enum from non-const inputs", () => {
|
|
|
87
87
|
|
|
88
88
|
test("get options", () => {
|
|
89
89
|
expect(z.enum(["tuna", "trout"]).options).toEqual(["tuna", "trout"]);
|
|
90
|
+
|
|
91
|
+
enum Fish {
|
|
92
|
+
Tuna = 0,
|
|
93
|
+
Trout = 1,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// numeric enums carry reverse-mapping keys; options lists only what parse accepts
|
|
97
|
+
expect(z.enum(Fish).options).toEqual([Fish.Tuna, Fish.Trout]);
|
|
98
|
+
expect(z.enum(Fish).safeParse("Tuna").success).toEqual(false);
|
|
90
99
|
});
|
|
91
100
|
|
|
92
101
|
test("readonly enum", () => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { inspect, types } from "node:util";
|
|
2
|
-
import
|
|
2
|
+
import v8 from "node:v8";
|
|
3
|
+
import vm from "node:vm";
|
|
4
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
5
|
import * as z from "zod/v4";
|
|
4
6
|
|
|
5
7
|
// Several tests install a global error map. Resetting on the last line of each leaks it into every later test in the file whenever an assertion above that line fails, which turns one real failure into a cascade.
|
|
@@ -977,3 +979,130 @@ test("error initializer never installs onto an intrinsic prototype", () => {
|
|
|
977
979
|
expect({}.toString()).toEqual("[object Object]");
|
|
978
980
|
expect(String(new Error("boom"))).toEqual("Error: boom");
|
|
979
981
|
});
|
|
982
|
+
|
|
983
|
+
describe("safeParse builds the error on first read", () => {
|
|
984
|
+
class CountingError {
|
|
985
|
+
static constructed = 0;
|
|
986
|
+
constructor(public issues: unknown[]) {
|
|
987
|
+
CountingError.constructed++;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
test("constructs once, on read", () => {
|
|
992
|
+
CountingError.constructed = 0;
|
|
993
|
+
const result = z.core._safeParse(CountingError as any)(z.string(), 1) as any;
|
|
994
|
+
expect(result.success).toBe(false);
|
|
995
|
+
expect(CountingError.constructed).toBe(0);
|
|
996
|
+
const err = result.error;
|
|
997
|
+
expect(CountingError.constructed).toBe(1);
|
|
998
|
+
expect(result.error).toBe(err);
|
|
999
|
+
expect(CountingError.constructed).toBe(1);
|
|
1000
|
+
expect(err.issues[0].code).toBe("invalid_type");
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
test("async variant", async () => {
|
|
1004
|
+
CountingError.constructed = 0;
|
|
1005
|
+
const result = (await z.core._safeParseAsync(CountingError as any)(z.string(), 1)) as any;
|
|
1006
|
+
expect(CountingError.constructed).toBe(0);
|
|
1007
|
+
expect(result.error).toBeInstanceOf(CountingError);
|
|
1008
|
+
expect(CountingError.constructed).toBe(1);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
test("result stays plain data", () => {
|
|
1012
|
+
const result = z.string().safeParse(1);
|
|
1013
|
+
expect(Object.keys(result)).toEqual(["success", "error"]);
|
|
1014
|
+
expect(result).toEqual({ success: false, error: expect.any(z.ZodError) });
|
|
1015
|
+
expect(structuredClone(result).error).toBeInstanceOf(Error);
|
|
1016
|
+
const replaced = new z.ZodError([]);
|
|
1017
|
+
(result as any).error = replaced;
|
|
1018
|
+
expect(result.error).toBe(replaced);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test("reading the error releases the parsed input", async () => {
|
|
1022
|
+
// es2020 is the target, and its lib predates WeakRef
|
|
1023
|
+
type Weak<T extends object> = { deref(): T | undefined };
|
|
1024
|
+
const { WeakRef: Weak } = globalThis as unknown as { WeakRef: new <T extends object>(target: T) => Weak<T> };
|
|
1025
|
+
// vitest carries no --expose-gc, so reach the collector the way node's own tests do
|
|
1026
|
+
v8.setFlagsFromString("--expose-gc");
|
|
1027
|
+
const gc = vm.runInNewContext("gc") as () => void;
|
|
1028
|
+
|
|
1029
|
+
const schema = z.string();
|
|
1030
|
+
const refs: Weak<object>[] = [];
|
|
1031
|
+
const results: unknown[] = [];
|
|
1032
|
+
for (let i = 0; i < 50; i++) {
|
|
1033
|
+
const input = { i, blob: new Array(500).fill(i) };
|
|
1034
|
+
refs.push(new Weak(input));
|
|
1035
|
+
const result = schema.safeParse(input);
|
|
1036
|
+
// half read the error, half replace it without ever reading; both have to release
|
|
1037
|
+
if (i % 2) void result.error;
|
|
1038
|
+
else (result as { error: z.core.$ZodError }).error = new z.ZodError([]);
|
|
1039
|
+
results.push(result);
|
|
1040
|
+
}
|
|
1041
|
+
// the collector decides when it runs, so give it several passes; a retained input never clears no matter how many it gets
|
|
1042
|
+
const live = () => refs.filter((ref) => ref.deref() !== undefined).length;
|
|
1043
|
+
for (let attempt = 0; attempt < 10 && live() > 1; attempt++) {
|
|
1044
|
+
// the loop's last input stays on the stack until a macrotask boundary
|
|
1045
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1046
|
+
gc();
|
|
1047
|
+
}
|
|
1048
|
+
expect(live()).toBeLessThanOrEqual(1);
|
|
1049
|
+
expect(results).toHaveLength(50);
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
test("no reader frames leak into the error, however late it is read", async () => {
|
|
1053
|
+
const result = z.string().safeParse(12) as { error: z.core.$ZodError };
|
|
1054
|
+
|
|
1055
|
+
// the constructor runs at THIS read: a macrotask later and twenty frames deeper than the parse
|
|
1056
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1057
|
+
const readDeep = <T>(n: number, make: () => T): T => (n ? readDeep(n - 1, make) : make());
|
|
1058
|
+
const error = readDeep(20, () => result.error) as unknown as Error;
|
|
1059
|
+
|
|
1060
|
+
// control: a plain Error at that same depth does carry the frames, so the assertions below discriminate rather than pass vacuously
|
|
1061
|
+
const control = readDeep(20, () => new Error("control"));
|
|
1062
|
+
expect(control.stack!.split("\n").some((line) => line.trim().startsWith("at "))).toBe(true);
|
|
1063
|
+
expect(control.stack!).toContain("readDeep");
|
|
1064
|
+
|
|
1065
|
+
expect(error.stack!.startsWith("ZodError: [")).toBe(true);
|
|
1066
|
+
expect(error.stack!.split("\n").some((line) => line.trim().startsWith("at "))).toBe(false);
|
|
1067
|
+
expect(error.stack!).not.toContain("readDeep");
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
test("a lazy build gives back the caller's stackTraceLimit, not the parse-time one", () => {
|
|
1071
|
+
const result = z.string().safeParse(12) as { error: z.core.$ZodError };
|
|
1072
|
+
const ambient = Error.stackTraceLimit;
|
|
1073
|
+
// changed between the parse and the read, so restoring a value captured at parse time would clobber it
|
|
1074
|
+
Error.stackTraceLimit = ambient + 7;
|
|
1075
|
+
try {
|
|
1076
|
+
void result.error;
|
|
1077
|
+
expect(Error.stackTraceLimit).toBe(ambient + 7);
|
|
1078
|
+
} finally {
|
|
1079
|
+
Error.stackTraceLimit = ambient;
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
test("a finalized issue copies the raw issue's own keys only", () => {
|
|
1085
|
+
const proto = { inherited: true };
|
|
1086
|
+
const schema = z.string().check((ctx) => {
|
|
1087
|
+
const raw = Object.create(proto);
|
|
1088
|
+
Object.assign(raw, { code: "custom", message: "own", input: ctx.value, extra: 1 });
|
|
1089
|
+
ctx.issues.push(raw);
|
|
1090
|
+
});
|
|
1091
|
+
const issue = schema.safeParse("x").error!.issues[0];
|
|
1092
|
+
expect(issue).toStrictEqual({ code: "custom", message: "own", path: [], extra: 1 });
|
|
1093
|
+
expect("inherited" in issue).toBe(false);
|
|
1094
|
+
// own symbol-keyed fields are not copied either; nothing in zod produces one and the walk stays a string-key walk
|
|
1095
|
+
const sym = Symbol("meta");
|
|
1096
|
+
const symbolic = z.string().check((ctx) => {
|
|
1097
|
+
ctx.issues.push({ code: "custom", message: "sym", input: ctx.value, [sym]: 1 } as never);
|
|
1098
|
+
});
|
|
1099
|
+
expect(Object.getOwnPropertySymbols(symbolic.safeParse("x").error!.issues[0])).toEqual([]);
|
|
1100
|
+
// an own __proto__ key, as JSON.parse produces, neither swaps the prototype nor survives
|
|
1101
|
+
const parsed = z.string().check((ctx) => {
|
|
1102
|
+
ctx.issues.push(JSON.parse('{"code":"custom","message":"json","__proto__":{"polluted":true}}'));
|
|
1103
|
+
});
|
|
1104
|
+
const fromJson = parsed.safeParse("x").error!.issues[0];
|
|
1105
|
+
expect(Object.getPrototypeOf(fromJson)).toBe(Object.prototype);
|
|
1106
|
+
expect(Object.prototype.hasOwnProperty.call(fromJson, "__proto__")).toBe(false);
|
|
1107
|
+
expect("polluted" in fromJson).toBe(false);
|
|
1108
|
+
});
|
|
@@ -80,6 +80,8 @@ test("first party switch", () => {
|
|
|
80
80
|
break;
|
|
81
81
|
case "file":
|
|
82
82
|
break;
|
|
83
|
+
case "properties":
|
|
84
|
+
break;
|
|
83
85
|
case "lazy":
|
|
84
86
|
break;
|
|
85
87
|
case "function":
|
|
@@ -170,6 +172,8 @@ test("$ZodSchemaTypes", () => {
|
|
|
170
172
|
break;
|
|
171
173
|
case "lazy":
|
|
172
174
|
break;
|
|
175
|
+
case "properties":
|
|
176
|
+
break;
|
|
173
177
|
case "function":
|
|
174
178
|
break;
|
|
175
179
|
|