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
|
@@ -63,14 +63,583 @@ test("validate agrees with the compiled fast path and keeps the callback bound",
|
|
|
63
63
|
|
|
64
64
|
calls = 0;
|
|
65
65
|
expect(z.validate(compiled, { name: "x" })).toBe(false);
|
|
66
|
+
// a user callback can throw, so its rejection is not decidable and the fallback still runs
|
|
66
67
|
expect(calls).toBeLessThanOrEqual(2);
|
|
67
68
|
|
|
68
69
|
expect(z.validate(compiled, { name: 42 })).toBe(false);
|
|
70
|
+
expect(z.validate(compiled, { name: "x" }, {})).toBe(false);
|
|
69
71
|
expect(z.validate(schema, { name: "ok" })).toBe(true);
|
|
70
72
|
});
|
|
71
73
|
|
|
74
|
+
test("the validate shortcut is taken for callback-free schemas only", () => {
|
|
75
|
+
const definite = (s: z.ZodType) => (z.compile(s, { strict: true }) as any)._zod.bag.validator?.definite;
|
|
76
|
+
|
|
77
|
+
// nothing here can throw, so a compiled rejection is proof the runtime would reject
|
|
78
|
+
expect(definite(z.object({ a: z.string().min(1), b: z.number().max(3), c: z.email() }))).toBe(true);
|
|
79
|
+
expect(definite(z.array(z.enum(["a", "b"])))).toBe(true);
|
|
80
|
+
|
|
81
|
+
// each of these can answer INVALID for something the interpreter throws on
|
|
82
|
+
expect(definite(z.object({ a: z.string().refine(() => true) }))).toBe(false);
|
|
83
|
+
expect(definite(z.string().transform((v) => v))).toBe(false);
|
|
84
|
+
expect(definite(z.custom(() => true))).toBe(false);
|
|
85
|
+
expect(definite(z.number().catch(0))).toBe(false);
|
|
86
|
+
expect(definite(z.lazy(() => z.string()))).toBe(false);
|
|
87
|
+
expect(
|
|
88
|
+
definite(
|
|
89
|
+
z.intersection(
|
|
90
|
+
z.number(),
|
|
91
|
+
z.number().transform((x) => x + 1)
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
).toBe(false);
|
|
95
|
+
expect(
|
|
96
|
+
definite(
|
|
97
|
+
z.record(
|
|
98
|
+
z.string().transform((k) => k),
|
|
99
|
+
z.number()
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("compiled validate keeps the fallback where INVALID is not a decidable rejection", () => {
|
|
106
|
+
// an intersection answers INVALID for an unmergeable merge, which the interpreter answers with a throw — a fast false would misreport a schema bug
|
|
107
|
+
const unmergeable = z.compile(
|
|
108
|
+
z.intersection(
|
|
109
|
+
z.number(),
|
|
110
|
+
z.number().transform((x) => x + 1)
|
|
111
|
+
),
|
|
112
|
+
{ strict: true }
|
|
113
|
+
);
|
|
114
|
+
expect(() => z.validate(unmergeable, 1234)).toThrow("Unmergable intersection");
|
|
115
|
+
|
|
116
|
+
// a when-gated check islands at codegen, and an island answers INVALID for an async run too
|
|
117
|
+
const gated = z.number().refine(() => Promise.resolve(true));
|
|
118
|
+
(gated._zod.def.checks![0]!._zod.def as { when?: unknown }).when = () => true;
|
|
119
|
+
const compiled = z.compile(z.object({ n: gated }), { strict: true });
|
|
120
|
+
expect(() => z.validate(compiled, { n: 3 })).toThrow(z.core.$ZodAsyncError);
|
|
121
|
+
expect(() => z.validate(z.object({ n: gated }), { n: 3 })).toThrow(z.core.$ZodAsyncError);
|
|
122
|
+
|
|
123
|
+
// a plain function handing back a promise answers INVALID for union parity, so a transform is never a decidable rejection
|
|
124
|
+
const piped = z.string().transform(() => Promise.resolve(1));
|
|
125
|
+
expect(() => z.validate(z.compile(piped, { strict: true }), "x")).toThrow(z.core.$ZodAsyncError);
|
|
126
|
+
expect(() => z.validate(piped, "x")).toThrow(z.core.$ZodAsyncError);
|
|
127
|
+
|
|
128
|
+
// a continuable child issue short-circuits the compiled intersection before its merge, but the interpreter reaches the merge and throws
|
|
129
|
+
const continuable = z.intersection(
|
|
130
|
+
z.number().min(10),
|
|
131
|
+
z.number().transform((x) => x + 1)
|
|
132
|
+
);
|
|
133
|
+
expect(() => z.validate(z.compile(continuable, { strict: true }), 1)).toThrow("Unmergable intersection");
|
|
134
|
+
expect(() => z.validate(continuable, 1)).toThrow("Unmergable intersection");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("compiled validate agrees with the interpreter, verdict and throw alike", () => {
|
|
138
|
+
// guards the `definite` flag: a new generator that answers INVALID for something the interpreter throws on must clear it, or this fails
|
|
139
|
+
const thenable = () => Promise.resolve(1) as never;
|
|
140
|
+
const schemas: z.ZodType[] = [
|
|
141
|
+
z.object({ a: z.string(), b: z.number().min(1) }),
|
|
142
|
+
z.strictObject({ a: z.string() }),
|
|
143
|
+
z.array(z.string().min(1)),
|
|
144
|
+
z.tuple([z.string()], z.number()),
|
|
145
|
+
z.record(z.string(), z.number()),
|
|
146
|
+
// a record key compiles in its own context, so its definiteness has to be carried out
|
|
147
|
+
z.record(z.string().transform(thenable), z.number()),
|
|
148
|
+
z.record(z.email(), z.number()),
|
|
149
|
+
z.union([z.string(), z.number()]),
|
|
150
|
+
z.intersection(
|
|
151
|
+
z.number(),
|
|
152
|
+
z.number().transform((x) => x + 1)
|
|
153
|
+
),
|
|
154
|
+
z.intersection(
|
|
155
|
+
z.number().min(10),
|
|
156
|
+
z.number().transform((x) => x + 1)
|
|
157
|
+
),
|
|
158
|
+
// unmergeable with no user callback in it, so only the intersection itself can clear the flag
|
|
159
|
+
z.intersection(z.string().default("a"), z.string().default("b")),
|
|
160
|
+
z.lazy(() => z.object({ a: z.string() })),
|
|
161
|
+
z.number().catch(0),
|
|
162
|
+
z.nonoptional(z.string().optional()),
|
|
163
|
+
z.map(z.string(), z.number()),
|
|
164
|
+
z.set(z.string().min(2)),
|
|
165
|
+
z.templateLiteral(["a", z.number()]),
|
|
166
|
+
z.coerce.number(),
|
|
167
|
+
z.stringbool(),
|
|
168
|
+
z.string().transform(thenable),
|
|
169
|
+
z.union([z.string().transform(thenable), z.number()]),
|
|
170
|
+
z.array(z.string().transform(thenable)),
|
|
171
|
+
z.custom(thenable),
|
|
172
|
+
z.string().refine(thenable),
|
|
173
|
+
z.string().pipe(z.string().min(3)),
|
|
174
|
+
// a codec's decode is the pipe branch's transform, and a plain function can still hand back a promise
|
|
175
|
+
z.codec(z.string(), z.number(), { decode: thenable, encode: String }),
|
|
176
|
+
z.object({ first: z.string(), second: z.codec(z.string(), z.number(), { decode: thenable, encode: String }) }),
|
|
177
|
+
// a continuable check does not settle the interpreter's walk, so it reaches a throwing callback the compiled path short-circuits past — the first-failure exit does not cover the check chain
|
|
178
|
+
z
|
|
179
|
+
.string()
|
|
180
|
+
.min(5)
|
|
181
|
+
.refine(() => {
|
|
182
|
+
throw new RangeError("u");
|
|
183
|
+
}),
|
|
184
|
+
z
|
|
185
|
+
.string()
|
|
186
|
+
.min(5)
|
|
187
|
+
.superRefine(() => {
|
|
188
|
+
throw new RangeError("u");
|
|
189
|
+
}),
|
|
190
|
+
// a user callback can throw, and compiled code can reject an earlier sibling before ever reaching it
|
|
191
|
+
z.object({
|
|
192
|
+
first: z.string(),
|
|
193
|
+
second: z.string().refine(() => {
|
|
194
|
+
throw new RangeError("u");
|
|
195
|
+
}),
|
|
196
|
+
}),
|
|
197
|
+
z.object({
|
|
198
|
+
first: z.string(),
|
|
199
|
+
second: z.custom(() => {
|
|
200
|
+
throw new TypeError("u");
|
|
201
|
+
}),
|
|
202
|
+
}),
|
|
203
|
+
// superRefine and check take the other branch of the refine generator than refine does
|
|
204
|
+
z.object({
|
|
205
|
+
first: z.string(),
|
|
206
|
+
second: z.string().superRefine(() => {
|
|
207
|
+
throw new RangeError("u");
|
|
208
|
+
}),
|
|
209
|
+
}),
|
|
210
|
+
z.string().superRefine(thenable as never),
|
|
211
|
+
z.object({
|
|
212
|
+
first: z.string(),
|
|
213
|
+
second: z.string().transform(() => {
|
|
214
|
+
throw new RangeError("u");
|
|
215
|
+
}),
|
|
216
|
+
}),
|
|
217
|
+
z.object({ first: z.string(), second: z.custom(thenable) }),
|
|
218
|
+
z.tuple([
|
|
219
|
+
z.string(),
|
|
220
|
+
z.number().catch(() => {
|
|
221
|
+
throw new RangeError("u");
|
|
222
|
+
}),
|
|
223
|
+
]),
|
|
224
|
+
];
|
|
225
|
+
const inputs = [
|
|
226
|
+
"x",
|
|
227
|
+
"ab",
|
|
228
|
+
"",
|
|
229
|
+
0,
|
|
230
|
+
1,
|
|
231
|
+
Number.NaN,
|
|
232
|
+
true,
|
|
233
|
+
null,
|
|
234
|
+
undefined,
|
|
235
|
+
{},
|
|
236
|
+
{ a: "x" },
|
|
237
|
+
{ a: 1 },
|
|
238
|
+
[],
|
|
239
|
+
["x"],
|
|
240
|
+
new Map(),
|
|
241
|
+
new Set(["a"]),
|
|
242
|
+
"true",
|
|
243
|
+
// the exposing shape: an earlier field the compiled path rejects before it reaches the throwing one
|
|
244
|
+
{ first: 1, second: "s" },
|
|
245
|
+
{ first: "s", second: "s" },
|
|
246
|
+
{ first: 1 },
|
|
247
|
+
[1, "x"],
|
|
248
|
+
["s", "x"],
|
|
249
|
+
];
|
|
250
|
+
const outcome = (fn: () => unknown) => {
|
|
251
|
+
try {
|
|
252
|
+
return `ok:${fn()}`;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return `throw:${(err as Error)?.constructor?.name}`;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
for (const schema of schemas) {
|
|
259
|
+
let compiled: z.ZodType;
|
|
260
|
+
try {
|
|
261
|
+
compiled = z.compile(schema, { strict: true });
|
|
262
|
+
} catch {
|
|
263
|
+
continue; // refused at codegen, so there is no fast path to disagree
|
|
264
|
+
}
|
|
265
|
+
for (const input of inputs) {
|
|
266
|
+
expect(
|
|
267
|
+
outcome(() => z.validate(compiled, input)),
|
|
268
|
+
`${JSON.stringify(input)} on ${schema._zod.def.type}`
|
|
269
|
+
).toBe(outcome(() => z.validate(schema, input)));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
72
274
|
test("validate is exported from zod/mini", () => {
|
|
73
275
|
expect(zm.validate(zm.string(), "a")).toBe(true);
|
|
74
276
|
expect(zm.validate(zm.string(), 1)).toBe(false);
|
|
75
277
|
expect(zm.validate(zm.object({ a: zm.number() }), { a: 1 })).toBe(true);
|
|
76
278
|
});
|
|
279
|
+
|
|
280
|
+
test("the validate method matches the top-level function", () => {
|
|
281
|
+
expect(z.string().validate("asdf")).toBe(true);
|
|
282
|
+
expect(z.string().validate(12)).toBe(false);
|
|
283
|
+
expect(z.object({ a: z.string() }).validate({ a: "x" })).toBe(true);
|
|
284
|
+
expect(z.object({ a: z.string() }).validate({ a: 1 })).toBe(false);
|
|
285
|
+
expect(z.compile(z.object({ a: z.string() })).validate({ a: "x" })).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("the validate method narrows to the input type", () => {
|
|
289
|
+
const value: unknown = "hi";
|
|
290
|
+
if (z.string().validate(value)) {
|
|
291
|
+
expectTypeOf(value).toEqualTypeOf<string>();
|
|
292
|
+
}
|
|
293
|
+
const transforming = z.string().transform((s) => s.length);
|
|
294
|
+
if (transforming.validate(value)) {
|
|
295
|
+
expectTypeOf(value).toEqualTypeOf<string>();
|
|
296
|
+
expectTypeOf(value).not.toEqualTypeOf<number>();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("the validate method takes a params argument", () => {
|
|
301
|
+
let calls = 0;
|
|
302
|
+
const error = () => {
|
|
303
|
+
calls++;
|
|
304
|
+
return "bad";
|
|
305
|
+
};
|
|
306
|
+
expect(z.string().validate(12, { error })).toBe(false);
|
|
307
|
+
expect(z.string().validate("ok", { error })).toBe(true);
|
|
308
|
+
// a ctx defeats the compiled definite shortcut, so this is the fallback answering
|
|
309
|
+
expect(z.compile(z.string()).validate(12, { error })).toBe(false);
|
|
310
|
+
expect(z.string().validate("ok", { jitless: true })).toBe(true);
|
|
311
|
+
// no issue is ever finalized, so an error map never runs
|
|
312
|
+
expect(calls).toBe(0);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("the validateAsync method handles async schemas the method form throws on", async () => {
|
|
316
|
+
const schema = z.string().refine(async (s) => s.length > 2);
|
|
317
|
+
expect(() => schema.validate("asdf")).toThrow(z.core.$ZodAsyncError);
|
|
318
|
+
await expect(schema.validateAsync("asdf")).resolves.toBe(true);
|
|
319
|
+
await expect(schema.validateAsync("a")).resolves.toBe(false);
|
|
320
|
+
await expect(z.string().validateAsync(12)).resolves.toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("zod/mini has no validate method", () => {
|
|
324
|
+
const schema = zm.string();
|
|
325
|
+
expect("parse" in schema).toBe(true);
|
|
326
|
+
expect("validate" in schema).toBe(false);
|
|
327
|
+
expect("validateAsync" in schema).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// counts reads of each child slot, so a stopped walk is observable without putting a callback in the schema — a refinement spy would work too, but reading the input says nothing about what ran
|
|
331
|
+
function counted(
|
|
332
|
+
slots: Record<string, unknown>,
|
|
333
|
+
bad: Record<string, unknown> = {}
|
|
334
|
+
): [Record<string, unknown>, () => number] {
|
|
335
|
+
let reads = 0;
|
|
336
|
+
const obj: Record<string, unknown> = { ...bad };
|
|
337
|
+
for (const [k, v] of Object.entries(slots)) {
|
|
338
|
+
Object.defineProperty(obj, k, {
|
|
339
|
+
enumerable: true,
|
|
340
|
+
get() {
|
|
341
|
+
reads++;
|
|
342
|
+
return v;
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return [obj, () => reads];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
test("validate stops a container at its first aborting issue", () => {
|
|
350
|
+
const shape = { a: z.number(), b: z.string(), c: z.string() };
|
|
351
|
+
|
|
352
|
+
for (const jitless of [false, true]) {
|
|
353
|
+
const label = jitless ? "interpreted" : "compiled";
|
|
354
|
+
const [input, reads] = counted({ b: "x", c: "x" }, { a: "no" });
|
|
355
|
+
expect(z.validate(z.object(shape), input, { jitless }), label).toBe(false);
|
|
356
|
+
expect(reads(), `${label}: keys read after the first abort`).toBe(0);
|
|
357
|
+
|
|
358
|
+
const [ok, okReads] = counted({ b: "x", c: "x" }, { a: 1 });
|
|
359
|
+
expect(z.validate(z.object(shape), ok, { jitless }), label).toBe(true);
|
|
360
|
+
expect(okReads(), `${label}: a valid parse still reads every key`).toBe(2);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// an array stops at the first bad element
|
|
364
|
+
const arr: unknown[] = [1];
|
|
365
|
+
let elementReads = 0;
|
|
366
|
+
for (const i of [1, 2]) {
|
|
367
|
+
Object.defineProperty(arr, i, {
|
|
368
|
+
enumerable: true,
|
|
369
|
+
get() {
|
|
370
|
+
elementReads++;
|
|
371
|
+
return "x";
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
expect(z.validate(z.array(z.string()), arr)).toBe(false);
|
|
376
|
+
expect(elementReads, "elements read after the first abort").toBe(0);
|
|
377
|
+
|
|
378
|
+
// the shape phase's abort carries across into the catchall
|
|
379
|
+
const [co, coReads] = counted({ b: "x" }, { a: "no" });
|
|
380
|
+
expect(z.validate(z.object({ a: z.number() }).catchall(z.string()), co)).toBe(false);
|
|
381
|
+
expect(coReads(), "catchall keys read after the shape aborted").toBe(0);
|
|
382
|
+
|
|
383
|
+
// and a tuple's fixed items carry into its rest
|
|
384
|
+
const tup: unknown[] = ["no"];
|
|
385
|
+
let restReads = 0;
|
|
386
|
+
for (const i of [1, 2]) {
|
|
387
|
+
Object.defineProperty(tup, i, {
|
|
388
|
+
enumerable: true,
|
|
389
|
+
get() {
|
|
390
|
+
restReads++;
|
|
391
|
+
return "x";
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
expect(z.validate(z.tuple([z.number()], z.string()), tup)).toBe(false);
|
|
396
|
+
expect(restReads, "rest elements read after a fixed item aborted").toBe(0);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("a default provider runs only when the key is absent", () => {
|
|
400
|
+
const boom = (): never => {
|
|
401
|
+
throw new RangeError("boom");
|
|
402
|
+
};
|
|
403
|
+
// the provider sits behind an accessor, so reading the def would call it
|
|
404
|
+
for (const [name, schema] of [
|
|
405
|
+
["default", z.object({ a: z.number(), b: z.string().default(boom as any) })],
|
|
406
|
+
["prefault", z.object({ a: z.number(), b: z.string().prefault(boom as any) })],
|
|
407
|
+
] as const) {
|
|
408
|
+
expect(z.validate(schema, { a: "bad", b: "ok" }), name).toBe(false);
|
|
409
|
+
expect(z.validate(schema, { a: 1, b: "ok" }), name).toBe(true);
|
|
410
|
+
}
|
|
411
|
+
// absent, the provider does run, and its throw is the parse's own
|
|
412
|
+
expect(() => z.validate(z.object({ b: z.string().default(boom as any) }), {})).toThrow(RangeError);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("the first failure stops the walk whatever follows it", () => {
|
|
416
|
+
// a sibling after the failure is never parsed, so its refinement never runs
|
|
417
|
+
let calls = 0;
|
|
418
|
+
const spy = z.string().refine(() => {
|
|
419
|
+
calls++;
|
|
420
|
+
return true;
|
|
421
|
+
});
|
|
422
|
+
expect(z.validate(z.object({ a: z.number(), b: spy, c: spy }), { a: "no", b: "x", c: "x" })).toBe(false);
|
|
423
|
+
expect(calls).toBe(0);
|
|
424
|
+
|
|
425
|
+
// reads of the trailing key say whether the walk stopped, without putting a callback in the schema
|
|
426
|
+
const skips = (b: z.ZodType, value: unknown = "x") => {
|
|
427
|
+
let reads = 0;
|
|
428
|
+
const input: Record<string, unknown> = { a: "no" };
|
|
429
|
+
Object.defineProperty(input, "b", {
|
|
430
|
+
enumerable: true,
|
|
431
|
+
get() {
|
|
432
|
+
reads++;
|
|
433
|
+
return value;
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
z.validate(z.object({ a: z.number(), b }), input);
|
|
437
|
+
return reads === 0;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// the trailing key is skipped no matter what it holds -- user code included, since it is never reached
|
|
441
|
+
for (const [name, b, value] of [
|
|
442
|
+
["plain", z.string(), "x"],
|
|
443
|
+
["min", z.string().min(2), "x"],
|
|
444
|
+
["email", z.email(), "x"],
|
|
445
|
+
["array", z.array(z.string().min(2)), ["ab"]],
|
|
446
|
+
["error map", z.string({ error: () => "nope" }), "x"],
|
|
447
|
+
["transform", z.string().transform((v) => v), "x"],
|
|
448
|
+
["refine", z.string().refine(() => true), "x"],
|
|
449
|
+
["custom", z.custom(() => true), "x"],
|
|
450
|
+
["catch", z.string().catch("x"), "x"],
|
|
451
|
+
["lazy", z.lazy(() => z.string()), "x"],
|
|
452
|
+
["coerce", z.coerce.number(), 1],
|
|
453
|
+
] as [string, z.ZodType, unknown][]) {
|
|
454
|
+
expect(skips(b, value), name).toBe(true);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("a continuable issue never stops the walk", () => {
|
|
459
|
+
// .min() is continuable, so a surrounding schema can still reconcile it and the guard must not stop
|
|
460
|
+
let calls = 0;
|
|
461
|
+
const el = z
|
|
462
|
+
.string()
|
|
463
|
+
.min(5)
|
|
464
|
+
.refine(() => {
|
|
465
|
+
calls++;
|
|
466
|
+
return true;
|
|
467
|
+
});
|
|
468
|
+
expect(z.validate(z.array(el), ["a", "b", "c"])).toBe(false);
|
|
469
|
+
expect(calls).toBe(3);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("z.record keeps walking under validate", () => {
|
|
473
|
+
// deliberate asymmetry: a record's invalid_key aborts, but an enclosing intersection reconciles it against the sibling operand, so a stopped loop hides keys the sibling does not own
|
|
474
|
+
let reads = 0;
|
|
475
|
+
const input: Record<string, unknown> = { a: 1 };
|
|
476
|
+
for (const k of ["b", "c"]) {
|
|
477
|
+
Object.defineProperty(input, k, {
|
|
478
|
+
enumerable: true,
|
|
479
|
+
get() {
|
|
480
|
+
reads++;
|
|
481
|
+
return "x";
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
expect(z.validate(z.record(z.string(), z.string()), input)).toBe(false);
|
|
486
|
+
expect(reads).toBe(2);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("validate matches safeParse where an issue can still be reconciled away", async () => {
|
|
490
|
+
const recA = z.record(z.string().regex(/^a/), z.any());
|
|
491
|
+
const recB = z.record(z.string().regex(/^b/), z.any());
|
|
492
|
+
const schemas: z.ZodType[] = [
|
|
493
|
+
z.intersection(recA, recB),
|
|
494
|
+
z.intersection(recA.pipe(z.any()), recB.pipe(z.any())),
|
|
495
|
+
z.intersection(z.strictObject({ a: z.string() }), z.strictObject({ b: z.number() })),
|
|
496
|
+
z.intersection(z.array(z.string()), z.array(z.string().min(2))),
|
|
497
|
+
z.strictObject({ a: z.string() }).pipe(z.any()),
|
|
498
|
+
z.union([z.strictObject({ a: z.string() }), z.strictObject({ a: z.string(), b: z.number() })]),
|
|
499
|
+
z.array(z.string()).catch([]),
|
|
500
|
+
z.object({ a: z.string() }).optional(),
|
|
501
|
+
];
|
|
502
|
+
const inputs: unknown[] = [
|
|
503
|
+
{ a1: 1, b1: 2 },
|
|
504
|
+
{ a1: 1, b1: 2, zz: 3 },
|
|
505
|
+
{ a1: 1, zz: 2, b1: 3, yy: 4 },
|
|
506
|
+
{ b1: 1, a1: 2, zz: 3, yy: 4 },
|
|
507
|
+
{ a: "x", b: 1 },
|
|
508
|
+
{ a: "x", b: 1, c: 9 },
|
|
509
|
+
{ a: "x" },
|
|
510
|
+
["ab", "cd"],
|
|
511
|
+
["a", "b"],
|
|
512
|
+
undefined,
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
const outcome = (fn: () => unknown) => {
|
|
516
|
+
try {
|
|
517
|
+
return `ok:${fn()}`;
|
|
518
|
+
} catch (err) {
|
|
519
|
+
return `throw:${(err as Error)?.constructor?.name}`;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
const outcomeAsync = async (fn: () => Promise<unknown>) => {
|
|
523
|
+
try {
|
|
524
|
+
return `ok:${await fn()}`;
|
|
525
|
+
} catch (err) {
|
|
526
|
+
return `throw:${(err as Error)?.constructor?.name}`;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
for (const schema of schemas) {
|
|
531
|
+
for (const input of inputs) {
|
|
532
|
+
const label = `${schema._zod.def.type} on ${JSON.stringify(input)}`;
|
|
533
|
+
expect(
|
|
534
|
+
outcome(() => z.validate(schema, input)),
|
|
535
|
+
label
|
|
536
|
+
).toBe(outcome(() => schema.safeParse(input).success));
|
|
537
|
+
expect(await outcomeAsync(() => z.validateAsync(schema, input)), label).toBe(
|
|
538
|
+
await outcomeAsync(async () => (await schema.safeParseAsync(input)).success)
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("abortEarly does not leak into safeParse", () => {
|
|
545
|
+
const schema = z.object({ a: z.string(), b: z.string(), c: z.string() });
|
|
546
|
+
expect(schema.safeParse({ a: 1, b: 2, c: 3 }).error!.issues).toHaveLength(3);
|
|
547
|
+
expect(z.validate(schema, { a: 1, b: 2, c: 3 })).toBe(false);
|
|
548
|
+
expect(schema.safeParse({ a: 1, b: 2, c: 3 }).error!.issues).toHaveLength(3);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("a callback after the first failure never runs, so validate answers where safeParse throws", () => {
|
|
552
|
+
const boom = (): never => {
|
|
553
|
+
throw new RangeError("boom");
|
|
554
|
+
};
|
|
555
|
+
// the deliberate divergence, in every shape that can carry a callback: an earlier sibling settles the boolean, so the later throw is never reached
|
|
556
|
+
const lateSym = Symbol("late");
|
|
557
|
+
const cases: [string, z.ZodType, unknown][] = [
|
|
558
|
+
["object transform", z.object({ a: z.number(), b: z.string().transform(boom) }), { a: "bad", b: "ok" }],
|
|
559
|
+
["object refine", z.object({ a: z.number(), b: z.string().refine(boom) }), { a: "bad", b: "ok" }],
|
|
560
|
+
["object custom", z.object({ a: z.number(), b: z.custom(boom) }), { a: "bad", b: "ok" }],
|
|
561
|
+
// these three hang the callback off `_zod.check` rather than the def
|
|
562
|
+
["object check", z.object({ a: z.number(), b: z.string().check(boom) }), { a: "bad", b: "ok" }],
|
|
563
|
+
["object superRefine", z.object({ a: z.number(), b: z.string().superRefine(boom) }), { a: "bad", b: "ok" }],
|
|
564
|
+
["object overwrite", z.object({ a: z.number(), b: z.string().overwrite(boom as any) }), { a: "bad", b: "ok" }],
|
|
565
|
+
// the input must carry the same symbol with a value the field accepts, or the transform never runs and the row asserts nothing
|
|
566
|
+
[
|
|
567
|
+
"symbol key",
|
|
568
|
+
z.object({ a: z.number(), [lateSym]: z.string().transform(boom) } as any),
|
|
569
|
+
{ a: "bad", [lateSym]: "ok" },
|
|
570
|
+
],
|
|
571
|
+
["object catch", z.object({ a: z.number(), b: z.string().catch(boom) }), { a: "bad", b: 1 }],
|
|
572
|
+
["object lazy", z.object({ a: z.number(), b: z.lazy(() => z.string().transform(boom)) }), { a: "bad", b: "ok" }],
|
|
573
|
+
["array", z.array(z.union([z.number(), z.string().transform(boom)])), [true, "throws"]],
|
|
574
|
+
["tuple rest", z.tuple([z.number()], z.string().transform(boom)), ["bad", "throws"]],
|
|
575
|
+
["catchall", z.object({ a: z.number() }).catchall(z.string().transform(boom)), { a: "bad", b: "ok" }],
|
|
576
|
+
["set", z.set(z.union([z.number(), z.string().transform(boom)])), new Set([true, "throws"])],
|
|
577
|
+
[
|
|
578
|
+
"map",
|
|
579
|
+
z.map(z.string(), z.union([z.number(), z.string().transform(boom)])),
|
|
580
|
+
new Map<any, any>([
|
|
581
|
+
[1, 0],
|
|
582
|
+
["k", "throws"],
|
|
583
|
+
]),
|
|
584
|
+
],
|
|
585
|
+
];
|
|
586
|
+
|
|
587
|
+
const outcome = (fn: () => unknown) => {
|
|
588
|
+
try {
|
|
589
|
+
return `ok:${fn()}`;
|
|
590
|
+
} catch (err) {
|
|
591
|
+
return `throw:${(err as Error)?.constructor?.name}`;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
for (const [name, schema, input] of cases) {
|
|
596
|
+
// safeParse walks the whole tree and reaches the throw; validate settled the answer at the earlier sibling and never runs it
|
|
597
|
+
expect(
|
|
598
|
+
outcome(() => schema.safeParse(input).success),
|
|
599
|
+
`${name}: safeParse`
|
|
600
|
+
).toBe("throw:RangeError");
|
|
601
|
+
expect(
|
|
602
|
+
outcome(() => z.validate(schema, input)),
|
|
603
|
+
`${name}: validate`
|
|
604
|
+
).toBe("ok:false");
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("the stop lands between children, not inside one", () => {
|
|
609
|
+
const boom = (): never => {
|
|
610
|
+
throw new RangeError("boom");
|
|
611
|
+
};
|
|
612
|
+
// both parse as a unit, so an abort in one half cannot skip the other; stopping inside either risks a discarded issue turning a false into a true
|
|
613
|
+
const pairs: [string, z.ZodType, unknown][] = [
|
|
614
|
+
["map entry", z.map(z.number(), z.string().transform(boom)), new Map([["bad", "ok"]])],
|
|
615
|
+
["tuple fixed item", z.tuple([z.number(), z.string().transform(boom)]), ["bad", "ok"]],
|
|
616
|
+
];
|
|
617
|
+
for (const [name, schema, input] of pairs) {
|
|
618
|
+
expect(() => z.validate(schema, input), name).toThrow(RangeError);
|
|
619
|
+
expect(() => schema.safeParse(input), `${name}: safeParse agrees`).toThrow(RangeError);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// between children, the stop does happen
|
|
623
|
+
expect(z.validate(z.object({ a: z.number(), b: z.string().transform(boom) }), { a: "bad", b: "ok" })).toBe(false);
|
|
624
|
+
expect(z.validate(z.array(z.union([z.number(), z.string().transform(boom)])), [true, "ok"])).toBe(false);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("validate rethrows a callback it actually reaches", () => {
|
|
628
|
+
const boom = (): never => {
|
|
629
|
+
throw new RangeError("boom");
|
|
630
|
+
};
|
|
631
|
+
// nothing rejects before the callback, so validate reaches it and the throw is the caller's
|
|
632
|
+
const cases: [string, z.ZodType, unknown][] = [
|
|
633
|
+
["transform", z.object({ a: z.string().transform(boom), b: z.number() }), { a: "ok", b: "bad" }],
|
|
634
|
+
["refine", z.object({ a: z.string().refine(boom), b: z.number() }), { a: "ok", b: "bad" }],
|
|
635
|
+
["check", z.object({ a: z.string().check(boom), b: z.number() }), { a: "ok", b: "bad" }],
|
|
636
|
+
["array element", z.array(z.string().transform(boom)), ["ok", "ok"]],
|
|
637
|
+
["catchall", z.object({}).catchall(z.string().transform(boom)), { a: "ok" }],
|
|
638
|
+
["set", z.set(z.string().transform(boom)), new Set(["ok"])],
|
|
639
|
+
["map", z.map(z.string(), z.string().transform(boom)), new Map([["k", "ok"]])],
|
|
640
|
+
["tuple rest", z.tuple([z.string()], z.string().transform(boom)), ["ok", "ok"]],
|
|
641
|
+
];
|
|
642
|
+
for (const [name, schema, input] of cases) {
|
|
643
|
+
expect(() => z.validate(schema, input), name).toThrow(RangeError);
|
|
644
|
+
}
|
|
645
|
+
});
|
package/src/v4/core/api.ts
CHANGED
|
@@ -498,6 +498,23 @@ export function _creditCard<T extends schemas.$ZodCreditCard>(
|
|
|
498
498
|
});
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
+
// IBAN
|
|
502
|
+
export type $ZodIBANParams = StringFormatParams<schemas.$ZodIBAN, "pattern" | "when">;
|
|
503
|
+
export type $ZodCheckIBANParams = CheckStringFormatParams<schemas.$ZodIBAN, "pattern" | "when">;
|
|
504
|
+
// @__NO_SIDE_EFFECTS__
|
|
505
|
+
export function _iban<T extends schemas.$ZodIBAN>(
|
|
506
|
+
Class: util.SchemaClass<T>,
|
|
507
|
+
params?: string | $ZodIBANParams | $ZodCheckIBANParams
|
|
508
|
+
): T {
|
|
509
|
+
return new Class({
|
|
510
|
+
type: "string",
|
|
511
|
+
format: "iban",
|
|
512
|
+
check: "string_format",
|
|
513
|
+
abort: false,
|
|
514
|
+
...util.normalizeParams(params),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
501
518
|
// JWT
|
|
502
519
|
export type $ZodJWTParams = StringFormatParams<schemas.$ZodJWT, "pattern" | "when">;
|
|
503
520
|
export type $ZodCheckJWTParams = CheckStringFormatParams<schemas.$ZodJWT, "pattern" | "when">;
|
|
@@ -1118,7 +1135,7 @@ export function _property<K extends string, T extends schemas.$ZodType>(
|
|
|
1118
1135
|
property: K,
|
|
1119
1136
|
schema: T,
|
|
1120
1137
|
params?: string | $ZodCheckPropertyParams
|
|
1121
|
-
): checks.$ZodCheckProperty<{ [k in K]: core.
|
|
1138
|
+
): checks.$ZodCheckProperty<{ [k in K]: util.Widen<core.input<T>> }> {
|
|
1122
1139
|
return new checks.$ZodCheckProperty({
|
|
1123
1140
|
check: "property",
|
|
1124
1141
|
property,
|
|
@@ -1127,13 +1144,20 @@ export function _property<K extends string, T extends schemas.$ZodType>(
|
|
|
1127
1144
|
});
|
|
1128
1145
|
}
|
|
1129
1146
|
|
|
1147
|
+
// `when` is omitted: it gates a check inside the run loop, so it means nothing when this is parsed as a schema, and honoring it in one role only would diverge silently
|
|
1148
|
+
export type $ZodPropertiesParams = CheckTypeParams<schemas.$ZodProperties, "shape" | "when">;
|
|
1130
1149
|
// @__NO_SIDE_EFFECTS__
|
|
1131
1150
|
export function _properties<Shape extends schemas.$ZodShape>(
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1151
|
+
Class: util.SchemaClass<schemas.$ZodProperties>,
|
|
1152
|
+
shape: Shape,
|
|
1153
|
+
params?: string | $ZodPropertiesParams
|
|
1154
|
+
): schemas.$ZodProperties<Shape> {
|
|
1155
|
+
return new Class({
|
|
1156
|
+
type: "properties",
|
|
1157
|
+
check: "properties",
|
|
1158
|
+
shape,
|
|
1159
|
+
...util.normalizeParams(params),
|
|
1160
|
+
}) as any;
|
|
1137
1161
|
}
|
|
1138
1162
|
|
|
1139
1163
|
export type $ZodCheckMimeTypeParams = CheckParams<checks.$ZodCheckMimeType, "mime" | "when">;
|