zod 4.5.4 → 4.6.0
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/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 +1 -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 +193 -81
- 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 +314 -83
- 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/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/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/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/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 +164 -65
- package/v4/core/compile.d.cts +18 -4
- package/v4/core/compile.d.ts +18 -4
- package/v4/core/compile.js +145 -66
- 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 +285 -66
- package/v4/core/schemas.d.cts +34 -3
- package/v4/core/schemas.d.ts +34 -3
- package/v4/core/schemas.js +264 -65
- 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 +19 -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/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
|
+
});
|
|
@@ -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
|
|
|
@@ -1308,3 +1308,160 @@ test("__proto__ annotation key reaches the registry", () => {
|
|
|
1308
1308
|
expect(Object.prototype.hasOwnProperty.call(meta, "__proto__")).toBe(true);
|
|
1309
1309
|
expect(meta.__proto__).toEqual({ custom: 1 });
|
|
1310
1310
|
});
|
|
1311
|
+
|
|
1312
|
+
test("minProperties and maxProperties count the raw input", () => {
|
|
1313
|
+
const schema = fromJSONSchema({
|
|
1314
|
+
type: "object",
|
|
1315
|
+
properties: { a: { type: "string" } },
|
|
1316
|
+
minProperties: 1,
|
|
1317
|
+
maxProperties: 2,
|
|
1318
|
+
});
|
|
1319
|
+
expect(schema.safeParse({ a: "x" }).success).toBe(true);
|
|
1320
|
+
expect(schema.safeParse({}).error!.issues[0]).toMatchObject({ code: "too_small", origin: "object", minimum: 1 });
|
|
1321
|
+
expect(schema.safeParse({ a: "x", b: 1, c: 2 }).error!.issues[0]).toMatchObject({
|
|
1322
|
+
code: "too_big",
|
|
1323
|
+
origin: "object",
|
|
1324
|
+
maximum: 2,
|
|
1325
|
+
});
|
|
1326
|
+
// raw input: a defaulted property does not satisfy the minimum, and a dropped __proto__ still counts
|
|
1327
|
+
const defaulted = fromJSONSchema({
|
|
1328
|
+
type: "object",
|
|
1329
|
+
properties: { a: { type: "string", default: "x" } },
|
|
1330
|
+
minProperties: 1,
|
|
1331
|
+
});
|
|
1332
|
+
expect(defaulted.safeParse({}).success).toBe(false);
|
|
1333
|
+
const capped = fromJSONSchema({ type: "object", maxProperties: 1 });
|
|
1334
|
+
expect(capped.safeParse(JSON.parse('{"__proto__":1,"a":1}')).success).toBe(false);
|
|
1335
|
+
expect(z.toJSONSchema(schema)).toMatchObject({ minProperties: 1, maxProperties: 2 });
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
test("minProperties composes with propertyNames in one guard", () => {
|
|
1339
|
+
const schema = fromJSONSchema({ type: "object", propertyNames: { pattern: "^a" }, minProperties: 1 });
|
|
1340
|
+
expect(schema.safeParse({}).error!.issues[0]!.code).toBe("too_small");
|
|
1341
|
+
expect(schema.safeParse({ b: 1 }).error!.issues[0]!.code).toBe("invalid_key");
|
|
1342
|
+
expect(schema.safeParse({ a: 1 }).success).toBe(true);
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
test("uniqueItems rejects structural duplicates", () => {
|
|
1346
|
+
const schema = fromJSONSchema({ type: "array", uniqueItems: true });
|
|
1347
|
+
expect(
|
|
1348
|
+
schema.safeParse([
|
|
1349
|
+
{ a: 1, b: 2 },
|
|
1350
|
+
{ b: 2, a: 1 },
|
|
1351
|
+
]).error!.issues[0]
|
|
1352
|
+
).toMatchObject({
|
|
1353
|
+
code: "custom",
|
|
1354
|
+
path: [1],
|
|
1355
|
+
});
|
|
1356
|
+
expect(schema.safeParse([1, "1", true, 1]).success).toBe(false);
|
|
1357
|
+
expect(schema.safeParse([{ a: 1 }, { a: 2 }, [1], [2]]).success).toBe(true);
|
|
1358
|
+
expect(z.toJSONSchema(schema)).toMatchObject({ uniqueItems: true });
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
test("contains with minContains and maxContains", () => {
|
|
1362
|
+
const schema = fromJSONSchema({ type: "array", contains: { type: "integer" } });
|
|
1363
|
+
expect(schema.safeParse(["a"]).success).toBe(false);
|
|
1364
|
+
expect(schema.safeParse(["a", 1]).success).toBe(true);
|
|
1365
|
+
const bounded = fromJSONSchema({ type: "array", contains: { type: "integer" }, minContains: 2, maxContains: 3 });
|
|
1366
|
+
expect(bounded.safeParse([1, "a"]).success).toBe(false);
|
|
1367
|
+
expect(bounded.safeParse([1, 2, "a"]).success).toBe(true);
|
|
1368
|
+
expect(bounded.safeParse([1, 2, 3, 4]).success).toBe(false);
|
|
1369
|
+
// minContains: 0 accepts an array with no matches at all
|
|
1370
|
+
expect(fromJSONSchema({ type: "array", contains: { type: "integer" }, minContains: 0 }).safeParse([]).success).toBe(
|
|
1371
|
+
true
|
|
1372
|
+
);
|
|
1373
|
+
expect(z.toJSONSchema(bounded)).toMatchObject({ contains: { type: "integer" }, minContains: 2, maxContains: 3 });
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
test("uniqueItems and contains see the raw instance, not the parsed output", () => {
|
|
1377
|
+
const items = { type: "object", properties: { a: { type: "string", default: "x" } } } as const;
|
|
1378
|
+
// no instance item carries `a`; the default must not synthesize the match
|
|
1379
|
+
const contains = fromJSONSchema({
|
|
1380
|
+
type: "array",
|
|
1381
|
+
items,
|
|
1382
|
+
contains: { type: "object", properties: { a: { const: "x" } }, required: ["a"] },
|
|
1383
|
+
});
|
|
1384
|
+
expect(contains.safeParse([{}]).success).toBe(false);
|
|
1385
|
+
expect(contains.safeParse([{ a: "x" }]).success).toBe(true);
|
|
1386
|
+
// the instance items differ; the default must not collapse them into duplicates
|
|
1387
|
+
const unique = fromJSONSchema({ type: "array", items, uniqueItems: true });
|
|
1388
|
+
expect(unique.safeParse([{ a: "x" }, {}]).success).toBe(true);
|
|
1389
|
+
expect(unique.safeParse([{}, {}]).error!.issues[0]).toMatchObject({ code: "custom", path: [1] });
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
test("a guard keyword takes precedence over the length keywords", () => {
|
|
1393
|
+
const schema = fromJSONSchema({ type: "array", items: { type: "integer" }, minItems: 3, uniqueItems: true });
|
|
1394
|
+
// the guard runs on the input side of the pipe, so its issue aborts before minItems is reached
|
|
1395
|
+
expect(schema.safeParse([1, 1]).error!.issues.map((i) => i.code)).toEqual(["custom"]);
|
|
1396
|
+
expect(schema.safeParse([1, 2]).error!.issues.map((i) => i.code)).toEqual(["too_small"]);
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
test("a guard keyword drops default and examples from the input-mode document", () => {
|
|
1400
|
+
// the guard is a transform on the pipe's input side, and toJSONSchema strips annotations from a transforming schema in input mode
|
|
1401
|
+
const schema = fromJSONSchema({ type: "array", uniqueItems: true, default: [], examples: [[1]] });
|
|
1402
|
+
expect(z.toJSONSchema(schema, { io: "input" })).not.toHaveProperty("default");
|
|
1403
|
+
expect(z.toJSONSchema(schema, { io: "output" })).toMatchObject({ default: [], examples: [[1]] });
|
|
1404
|
+
// without a guard keyword both modes carry them
|
|
1405
|
+
const plain = fromJSONSchema({ type: "array", default: [], examples: [[1]] });
|
|
1406
|
+
expect(z.toJSONSchema(plain, { io: "input" })).toMatchObject({ default: [], examples: [[1]] });
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
test("uniqueItems equality follows JSON semantics", () => {
|
|
1410
|
+
const schema = fromJSONSchema({ type: "array", uniqueItems: true });
|
|
1411
|
+
expect(schema.safeParse([1, "1", true, null]).success).toBe(true);
|
|
1412
|
+
expect(schema.safeParse([{ a: undefined }, {}]).success).toBe(true);
|
|
1413
|
+
expect(
|
|
1414
|
+
schema.safeParse([
|
|
1415
|
+
[1, [2]],
|
|
1416
|
+
[1, [2]],
|
|
1417
|
+
]).success
|
|
1418
|
+
).toBe(false);
|
|
1419
|
+
expect(schema.safeParse([{ a: "b=c" }, { "a=b": "c" }]).success).toBe(true);
|
|
1420
|
+
// every duplicate is reported against the first element it matches
|
|
1421
|
+
expect(schema.safeParse([1, 1, 1]).error!.issues.map((i) => i.path[0])).toEqual([1, 2]);
|
|
1422
|
+
// a cycle cannot be compared, so it is never called a duplicate rather than hanging
|
|
1423
|
+
const cyclic: any = {};
|
|
1424
|
+
cyclic.self = cyclic;
|
|
1425
|
+
expect(schema.safeParse([cyclic, cyclic]).success).toBe(true);
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
test("a carried subschema that references $defs is dropped rather than emitted dangling", () => {
|
|
1429
|
+
const schema = fromJSONSchema({
|
|
1430
|
+
type: "array",
|
|
1431
|
+
contains: { $ref: "#/$defs/Hit" },
|
|
1432
|
+
$defs: { Hit: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
1433
|
+
});
|
|
1434
|
+
// enforcement is unaffected; only the round trip gives the keyword up
|
|
1435
|
+
expect(schema.safeParse([{ id: "a" }]).success).toBe(true);
|
|
1436
|
+
expect(schema.safeParse([{}]).success).toBe(false);
|
|
1437
|
+
expect(JSON.stringify(z.toJSONSchema(schema))).not.toContain("$ref");
|
|
1438
|
+
expect(z.toJSONSchema(schema)).not.toHaveProperty("contains");
|
|
1439
|
+
// an inline subschema is self-contained, so it still round-trips
|
|
1440
|
+
const inline = fromJSONSchema({ type: "array", contains: { type: "integer" } });
|
|
1441
|
+
expect(z.toJSONSchema(inline)).toMatchObject({ contains: { type: "integer" } });
|
|
1442
|
+
// only schema positions are walked, so a `$ref` key in instance data or an unknown annotation still travels
|
|
1443
|
+
for (const annotation of [{ default: { $ref: "literal" } }, { "x-note": { $ref: "literal" } }]) {
|
|
1444
|
+
const annotated = fromJSONSchema({ type: "array", contains: { type: "integer", ...annotation } });
|
|
1445
|
+
expect(annotated.safeParse(["x"]).success).toBe(false);
|
|
1446
|
+
expect(z.toJSONSchema(annotated)).toMatchObject({ contains: { type: "integer" } });
|
|
1447
|
+
}
|
|
1448
|
+
// draft-7 `dependencies` is a schema map; its array form names properties and holds no reference
|
|
1449
|
+
const dependent = fromJSONSchema({
|
|
1450
|
+
type: "array",
|
|
1451
|
+
contains: { type: "object", dependencies: { a: { $ref: "#/definitions/Hit" } } },
|
|
1452
|
+
definitions: { Hit: { type: "object" } },
|
|
1453
|
+
});
|
|
1454
|
+
expect(z.toJSONSchema(dependent)).not.toHaveProperty("contains");
|
|
1455
|
+
const named = fromJSONSchema({
|
|
1456
|
+
type: "array",
|
|
1457
|
+
contains: { type: "object", dependencies: { a: ["b"] } },
|
|
1458
|
+
});
|
|
1459
|
+
expect(z.toJSONSchema(named)).toMatchObject({ contains: { dependencies: { a: ["b"] } } });
|
|
1460
|
+
// a reference nested in a schema position is still caught
|
|
1461
|
+
const nested = fromJSONSchema({
|
|
1462
|
+
type: "array",
|
|
1463
|
+
contains: { type: "array", items: { $ref: "#/$defs/Hit" } },
|
|
1464
|
+
$defs: { Hit: { type: "string" } },
|
|
1465
|
+
});
|
|
1466
|
+
expect(z.toJSONSchema(nested)).not.toHaveProperty("contains");
|
|
1467
|
+
});
|