zod 4.5.3 → 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 +316 -0
- package/src/v4/classic/tests/default.test.ts +46 -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 +162 -36
- 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 +204 -36
- package/v4/core/memoizer.js +162 -36
- 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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { expect, expectTypeOf, test } from "vitest";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
|
|
4
|
+
test("z.properties is a schema", () => {
|
|
5
|
+
const p = z.properties({ a: z.literal("x"), b: z.literal("y") });
|
|
6
|
+
|
|
7
|
+
expectTypeOf<z.infer<typeof p>>().toEqualTypeOf<{ a: "x"; b: "y" }>();
|
|
8
|
+
|
|
9
|
+
// asserts in place: the parse returns the input identity, extra keys pass
|
|
10
|
+
const ok = { a: "x", b: "y", extra: 1 };
|
|
11
|
+
expect(p.parse(ok)).toBe(ok);
|
|
12
|
+
|
|
13
|
+
// every failing key reports
|
|
14
|
+
expect(p.safeParse({ a: "!", b: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"], ["b"]]);
|
|
15
|
+
|
|
16
|
+
// as a schema it is the type gate and infers an object shape, so a primitive is a type error
|
|
17
|
+
for (const input of [null, undefined, 5, "abc"]) {
|
|
18
|
+
expect(p.safeParse(input).error!.issues.map((i) => [i.code, i.path])).toEqual([["invalid_type", []]]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// as a check the base already typed the value, so the properties are asserted on whatever it produced — a string's length, as z.property() does
|
|
22
|
+
const long = z.string().check(...z.properties({ length: z.number().min(3) }));
|
|
23
|
+
expect(long.parse("abc")).toBe("abc");
|
|
24
|
+
expect(long.safeParse("ab").error!.issues.map((i) => i.path)).toEqual([["length"]]);
|
|
25
|
+
expect(
|
|
26
|
+
z
|
|
27
|
+
.compile(long)
|
|
28
|
+
.safeParse("ab")
|
|
29
|
+
.error!.issues.map((i) => i.path)
|
|
30
|
+
).toEqual([["length"]]);
|
|
31
|
+
|
|
32
|
+
// an optional key may be absent
|
|
33
|
+
const opt = z.properties({ a: z.string().optional() });
|
|
34
|
+
expect(opt.safeParse({}).success).toBe(true);
|
|
35
|
+
expectTypeOf<z.infer<typeof opt>>().toEqualTypeOf<{ a?: string | undefined }>();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("z.properties discards transformed output", () => {
|
|
39
|
+
// asserts and never writes back, exactly as z.property() behaves; a nested object schema rebuilds its output even without transforms, so identity cannot distinguish the two
|
|
40
|
+
const p = z.properties({ a: z.string().transform((s) => s.toUpperCase()) });
|
|
41
|
+
const input = { a: "hi" };
|
|
42
|
+
expect(p.parse(input)).toBe(input);
|
|
43
|
+
expect(input.a).toBe("hi");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("z.properties narrows through intersection", () => {
|
|
47
|
+
const httpsUrl = z.instanceof(URL).and(z.properties({ protocol: z.literal("https:") }));
|
|
48
|
+
expectTypeOf<z.infer<typeof httpsUrl>>().toEqualTypeOf<URL & { protocol: "https:" }>();
|
|
49
|
+
|
|
50
|
+
const u = new URL("https://example.com");
|
|
51
|
+
expect(httpsUrl.parse(u)).toBe(u);
|
|
52
|
+
expect(httpsUrl.safeParse(new URL("http://example.com")).success).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("z.instanceof().properties()", () => {
|
|
56
|
+
const httpsUrl = z.instanceof(URL).properties({ protocol: z.literal("https:") });
|
|
57
|
+
expectTypeOf<z.infer<typeof httpsUrl>>().toEqualTypeOf<URL & { protocol: "https:" }>();
|
|
58
|
+
|
|
59
|
+
const u = new URL("https://example.com");
|
|
60
|
+
expect(httpsUrl.parse(u)).toBe(u);
|
|
61
|
+
expect(httpsUrl.safeParse(new URL("http://example.com")).error!.issues.map((i) => i.path)).toEqual([["protocol"]]);
|
|
62
|
+
|
|
63
|
+
// chains, and each call narrows further
|
|
64
|
+
const chained = httpsUrl.properties({ port: z.literal("") });
|
|
65
|
+
expectTypeOf<z.infer<typeof chained>>().toEqualTypeOf<URL & { protocol: "https:" } & { port: "" }>();
|
|
66
|
+
expect(chained.safeParse(u).success).toBe(true);
|
|
67
|
+
expect(chained.safeParse(new URL("https://example.com:8443")).error!.issues.map((i) => i.path)).toEqual([["port"]]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("z.properties spreads into .check()", () => {
|
|
71
|
+
// the pre-4.6 array call sites: the schema yields itself from Symbol.iterator
|
|
72
|
+
const p = z.properties({ a: z.literal("x") });
|
|
73
|
+
const spread = [...p];
|
|
74
|
+
expect(spread).toEqual([p]);
|
|
75
|
+
|
|
76
|
+
const s = z.object({ a: z.string() }).check(...z.properties({ a: z.literal("x") }));
|
|
77
|
+
expect(s.safeParse({ a: "x" }).success).toBe(true);
|
|
78
|
+
expect(s.safeParse({ a: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"]]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("z.properties JSON Schema", () => {
|
|
82
|
+
const p = z.properties({ a: z.string(), b: z.number().optional() });
|
|
83
|
+
expect(z.toJSONSchema(p)).toEqual({
|
|
84
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: { a: { type: "string" }, b: { type: "number" } },
|
|
87
|
+
required: ["a"],
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("z.properties compiles", () => {
|
|
92
|
+
const p = z.properties({ a: z.literal("x"), b: z.literal("y") });
|
|
93
|
+
const compiled = z.compile(p);
|
|
94
|
+
const ok = { a: "x", b: "y", extra: 1 };
|
|
95
|
+
expect(compiled.parse(ok)).toBe(ok);
|
|
96
|
+
expect(compiled.safeParse({ a: "!", b: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"], ["b"]]);
|
|
97
|
+
expect(compiled.safeParse(null).error!.issues.map((i) => [i.code, i.path])).toEqual([["invalid_type", []]]);
|
|
98
|
+
|
|
99
|
+
// nested in a container
|
|
100
|
+
const outer = z.compile(z.object({ url: z.instanceof(URL).properties({ protocol: z.literal("https:") }) }));
|
|
101
|
+
expect(outer.safeParse({ url: new URL("http://example.com") }).error!.issues.map((i) => i.path)).toEqual([
|
|
102
|
+
["url", "protocol"],
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("z.properties async", async () => {
|
|
107
|
+
const p = z.properties({ a: z.string().refine(async (s) => s.length > 1) });
|
|
108
|
+
const ok = { a: "hi" };
|
|
109
|
+
await expect(p.parseAsync(ok)).resolves.toBe(ok);
|
|
110
|
+
const bad = await p.safeParseAsync({ a: "!" });
|
|
111
|
+
expect(bad.error!.issues.map((i) => i.path)).toEqual([["a"]]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("z.properties infers the input side", () => {
|
|
115
|
+
// nothing is written back, so an output-typed inference would lie for every shape entry whose output differs from its input
|
|
116
|
+
const withDefault = z.properties({ a: z.string().default("x") });
|
|
117
|
+
expectTypeOf<z.infer<typeof withDefault>>().toEqualTypeOf<{ a?: string | undefined }>();
|
|
118
|
+
expect(withDefault.parse({})).toEqual({});
|
|
119
|
+
|
|
120
|
+
const withTransform = z.properties({ a: z.string().transform((s) => s.length) });
|
|
121
|
+
expectTypeOf<z.infer<typeof withTransform>>().toEqualTypeOf<{ a: string }>();
|
|
122
|
+
expect(withTransform.parse({ a: "hi" })).toEqual({ a: "hi" });
|
|
123
|
+
|
|
124
|
+
// a plain schema has the same input and output, so the documented narrowing is unchanged
|
|
125
|
+
const W = z.instanceof(URL).properties({ protocol: z.literal("https:") });
|
|
126
|
+
expectTypeOf<z.infer<typeof W>>().toEqualTypeOf<URL & { protocol: "https:" }>();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("z.properties validates symbol keys", () => {
|
|
130
|
+
const sym = Symbol("tag");
|
|
131
|
+
const p = z.properties({ [sym]: z.string() });
|
|
132
|
+
expect(p.safeParse({ [sym]: "ok" }).success).toBe(true);
|
|
133
|
+
expect(p.safeParse({ [sym]: 123 }).error!.issues.map((i) => i.path)).toEqual([[sym]]);
|
|
134
|
+
expect(
|
|
135
|
+
z
|
|
136
|
+
.compile(p)
|
|
137
|
+
.safeParse({ [sym]: 123 })
|
|
138
|
+
.error!.issues.map((i) => i.path)
|
|
139
|
+
).toEqual([[sym]]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("z.properties compiled and interpreted agree on a nullish value", () => {
|
|
143
|
+
// a base that permits null reaches the check role with one; the compiled property read must not throw where the runtime reports an issue
|
|
144
|
+
const s = z.any().check(...z.properties({ a: z.string() }));
|
|
145
|
+
const expected = [["invalid_type", []]];
|
|
146
|
+
expect(s.safeParse(null).error!.issues.map((i) => [i.code, i.path])).toEqual(expected);
|
|
147
|
+
expect(
|
|
148
|
+
z
|
|
149
|
+
.compile(s)
|
|
150
|
+
.safeParse(null)
|
|
151
|
+
.error!.issues.map((i) => [i.code, i.path])
|
|
152
|
+
).toEqual(expected);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("z.properties with a custom when refuses to compile", () => {
|
|
156
|
+
// `when` is not publicly settable, since it gates a check inside the run loop and means nothing to a parsed schema. A hand-built def can still carry one, and compiling it must refuse rather than run the shape unconditionally: a union branch compiles to its own IIFE, so a wrong rejection is absorbed as a branch failure with no fallback.
|
|
157
|
+
const p = new z.core.$ZodProperties({
|
|
158
|
+
type: "properties",
|
|
159
|
+
check: "properties",
|
|
160
|
+
shape: { a: z.literal("x") },
|
|
161
|
+
when: () => false,
|
|
162
|
+
});
|
|
163
|
+
const s = z.union([p as unknown as z.ZodType, z.object({ b: z.string() })]);
|
|
164
|
+
const input = { a: "wrong", b: "hello" };
|
|
165
|
+
expect(z.compile(s).safeParse(input)).toEqual(s.safeParse(input));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("z.properties parses cyclic input", () => {
|
|
169
|
+
// the input is its own output, so it registers as its own memo entry; without that a cycle re-enters forever
|
|
170
|
+
const Node: z.ZodType<{ id: string; next?: unknown }> = z.lazy(() =>
|
|
171
|
+
z.properties({ id: z.string(), next: Node.optional() })
|
|
172
|
+
);
|
|
173
|
+
const cyclic: Record<string, unknown> = { id: "a" };
|
|
174
|
+
cyclic.next = cyclic;
|
|
175
|
+
expect(Node.parse(cyclic)).toBe(cyclic);
|
|
176
|
+
expect(z.compile(Node).parse(cyclic)).toBe(cyclic);
|
|
177
|
+
|
|
178
|
+
// the cycle guard must not swallow a real failure further down
|
|
179
|
+
expect(Node.safeParse({ id: "a", next: { id: 1 } }).error!.issues.map((i) => i.path)).toEqual([["next", "id"]]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("z.properties asserts forward while encoding", () => {
|
|
183
|
+
// both sides declare the shape's input type, so encoding must still check the value against it; running the children backward would encode them and reject that type
|
|
184
|
+
const codec = z.codec(z.string(), z.number(), { decode: (s) => Number(s), encode: (n) => String(n) });
|
|
185
|
+
const p = z.properties({ a: codec, b: z.string().transform((s) => s.length) });
|
|
186
|
+
const value = { a: "1", b: "hi" };
|
|
187
|
+
expect(z.decode(p, value)).toBe(value);
|
|
188
|
+
expect(z.encode(p, value)).toBe(value);
|
|
189
|
+
expect(z.safeEncode(p, { a: 1, b: "hi" } as never).error!.issues.map((i) => i.path)).toEqual([["a"]]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("z.properties parses a cyclic callable", () => {
|
|
193
|
+
// property reads work on a function too, so the cycle guard has to recognize one as a reference
|
|
194
|
+
const Fn: z.ZodType<{ self?: unknown }> = z.lazy(() => z.properties({ self: Fn.optional() }));
|
|
195
|
+
const fn = (() => {}) as unknown as Record<string, unknown>;
|
|
196
|
+
fn.self = fn;
|
|
197
|
+
expect(Fn.parse(fn)).toBe(fn);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("z.properties survives shape mutation", () => {
|
|
201
|
+
// key and schema are snapshotted together; caching one and reading the other live paired a stale key with a missing schema
|
|
202
|
+
const shape: Record<string, z.ZodType> = { a: z.string() };
|
|
203
|
+
const p = z.properties(shape);
|
|
204
|
+
expect(p.safeParse({ a: "x" }).success).toBe(true);
|
|
205
|
+
delete shape.a;
|
|
206
|
+
expect(p.safeParse({ a: "x" }).success).toBe(true);
|
|
207
|
+
shape.b = z.string();
|
|
208
|
+
expect(p.safeParse({ a: "x" }).success).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("z.properties JSON Schema describes the input side", () => {
|
|
212
|
+
// the parsed value is the input, so a defaulted key that stays absent must not be required of the output either
|
|
213
|
+
const p = z.properties({ a: z.string().default("x") });
|
|
214
|
+
expect(p.parse({})).toEqual({});
|
|
215
|
+
expect(z.toJSONSchema(p, { io: "output" })).toEqual({
|
|
216
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: { a: { default: "x", type: "string" } },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// an output mode emission for a transforming child would describe a value this schema never returns
|
|
222
|
+
const t = z.properties({ a: z.string().transform((s) => s.length) });
|
|
223
|
+
expect(() => z.toJSONSchema(t, { io: "output" })).toThrow(/cannot be represented/);
|
|
224
|
+
expect(z.toJSONSchema(t, { io: "input" })).toMatchObject({ properties: { a: { type: "string" } }, required: ["a"] });
|
|
225
|
+
});
|
|
@@ -556,7 +556,7 @@ test("recursive object with .check()", () => {
|
|
|
556
556
|
expect(() => Category.parse(invalidData)).toThrow();
|
|
557
557
|
});
|
|
558
558
|
|
|
559
|
-
// biome-ignore lint:
|
|
559
|
+
// biome-ignore lint/suspicious/noExportsInTest: unexported it trips noUnusedVariables instead
|
|
560
560
|
export type RecursiveA = z.ZodUnion<
|
|
561
561
|
[
|
|
562
562
|
z.ZodObject<{
|
|
@@ -132,3 +132,22 @@ test("z.toJSONSchema() returns StandardJSONSchemaV1", async () => {
|
|
|
132
132
|
}
|
|
133
133
|
`);
|
|
134
134
|
});
|
|
135
|
+
|
|
136
|
+
test("a failing validate reports the same issues as safeParse, without a ZodError", async () => {
|
|
137
|
+
const schema = z.object({ name: z.string().min(3), age: z.number().refine(async (n) => n > 0) });
|
|
138
|
+
const input = { name: "ab", age: -1 };
|
|
139
|
+
const result = (await schema["~standard"].validate(input)) as { issues: unknown[] };
|
|
140
|
+
expect(result.issues).toEqual((await schema.safeParseAsync(input)).error!.issues);
|
|
141
|
+
expect(result).not.toHaveProperty("error");
|
|
142
|
+
const sync = z.string().min(3)["~standard"].validate("ab") as { issues: unknown[] };
|
|
143
|
+
expect(sync.issues).toEqual(z.string().min(3).safeParse("ab").error!.issues);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("a synchronously throwing check rejects through ~standard instead of throwing", async () => {
|
|
147
|
+
const schema = z.string().refine(() => {
|
|
148
|
+
throw new Error("boom");
|
|
149
|
+
});
|
|
150
|
+
const result = schema["~standard"].validate("abc");
|
|
151
|
+
expect(result).toBeInstanceOf(Promise);
|
|
152
|
+
await expect(result as Promise<unknown>).rejects.toThrow("boom");
|
|
153
|
+
});
|
|
@@ -264,6 +264,7 @@ test("base64url validations", () => {
|
|
|
264
264
|
|
|
265
265
|
const invalidBase64URLStrings = [
|
|
266
266
|
"w7/Dv8O+w74K", // Has + and / characters (is base64)
|
|
267
|
+
"A", // Invalid length (1 mod 4 carries no whole byte)
|
|
267
268
|
"12345", // Invalid length (not a multiple of 4 characters when adding allowed number of padding characters)
|
|
268
269
|
"12345===", // Not padded correctly
|
|
269
270
|
"!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!'
|
|
@@ -291,6 +292,18 @@ test("big base64 and base64url", () => {
|
|
|
291
292
|
z.base64().parse(bigbase64);
|
|
292
293
|
const bigbase64url = randomBytes(1024 * 1024 * 10).toString("base64url");
|
|
293
294
|
z.base64url().parse(bigbase64url);
|
|
295
|
+
|
|
296
|
+
// template literals compose def.pattern and test it on every parse; the quantified block forms overflowed the regex stack here
|
|
297
|
+
expect(z.templateLiteral(["id-", z.base64()]).safeParse(`id-${bigbase64}`).success).toBe(true);
|
|
298
|
+
expect(z.templateLiteral(["id-", z.base64url()]).safeParse(`id-${bigbase64url}`).success).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("template literal composes base64 without alternation leaks", () => {
|
|
302
|
+
// the ^$| alternation in regexes.base64 used to leak: "AAAA" matched with the prefix dropped and "id-AAAA" was rejected
|
|
303
|
+
const tl = z.templateLiteral(["id-", z.base64()]);
|
|
304
|
+
expect(tl.safeParse("id-AAAA").success).toBe(true);
|
|
305
|
+
expect(tl.safeParse("id-").success).toBe(true);
|
|
306
|
+
expect(tl.safeParse("AAAA").success).toBe(false);
|
|
294
307
|
});
|
|
295
308
|
|
|
296
309
|
function makeJwt(header: object, payload: object) {
|
|
@@ -552,6 +565,18 @@ test("emoji validations", () => {
|
|
|
552
565
|
expect(() => emoji.parse("😀 is an emoji")).toThrow();
|
|
553
566
|
expect(() => emoji.parse("😀stuff")).toThrow();
|
|
554
567
|
expect(() => emoji.parse("stuff😀")).toThrow();
|
|
568
|
+
|
|
569
|
+
emoji.parse("1\u{FE0F}\u{20E3}"); // keycap sequences stay valid
|
|
570
|
+
emoji.parse("#\u{FE0F}\u{20E3}");
|
|
571
|
+
|
|
572
|
+
// `\p{Emoji_Component}` alone covers ASCII digits, "#", "*", ZWJ, variation selectors and skin tone modifiers; none is an emoji without a base, so a component-only string is not one either
|
|
573
|
+
expect(() => emoji.parse("123")).toThrow();
|
|
574
|
+
expect(() => emoji.parse("#")).toThrow();
|
|
575
|
+
expect(() => emoji.parse("*")).toThrow();
|
|
576
|
+
expect(() => emoji.parse("\u{200D}")).toThrow();
|
|
577
|
+
expect(() => emoji.parse("\u{FE0F}")).toThrow();
|
|
578
|
+
expect(() => emoji.parse("\u{1F3FD}")).toThrow();
|
|
579
|
+
expect(() => emoji.parse("1\u{FE0F}")).toThrow();
|
|
555
580
|
});
|
|
556
581
|
|
|
557
582
|
test("nanoid", () => {
|
|
@@ -565,7 +565,7 @@ test("regexes", () => {
|
|
|
565
565
|
`"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"`
|
|
566
566
|
);
|
|
567
567
|
expect(email._zod.pattern.source).toMatchInlineSnapshot(
|
|
568
|
-
`"^(
|
|
568
|
+
`"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"`
|
|
569
569
|
);
|
|
570
570
|
// expect(ip._zod.pattern.source).toMatchInlineSnapshot(
|
|
571
571
|
// `"^(^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$)|(^(([a-fA-F0-9]{1,4}:){7}|::([a-fA-F0-9]{1,4}:){0,6}|([a-fA-F0-9]{1,4}:){1}:([a-fA-F0-9]{1,4}:){0,5}|([a-fA-F0-9]{1,4}:){2}:([a-fA-F0-9]{1,4}:){0,4}|([a-fA-F0-9]{1,4}:){3}:([a-fA-F0-9]{1,4}:){0,3}|([a-fA-F0-9]{1,4}:){4}:([a-fA-F0-9]{1,4}:){0,2}|([a-fA-F0-9]{1,4}:){5}:([a-fA-F0-9]{1,4}:){0,1})([a-fA-F0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$)$"`
|
|
@@ -778,3 +778,35 @@ test("template literal parsing - failure - issue format", () => {
|
|
|
778
778
|
}
|
|
779
779
|
`);
|
|
780
780
|
});
|
|
781
|
+
|
|
782
|
+
test("part patterns fold checks through wrappers and unions", () => {
|
|
783
|
+
// a constrained string keeps its bounds under a wrapper and inside a union
|
|
784
|
+
const wrapped = z.templateLiteral(["a-", z.string().min(2).max(3).optional()]);
|
|
785
|
+
expect(wrapped.safeParse("a-").success).toBe(true);
|
|
786
|
+
expect(wrapped.safeParse("a-xy").success).toBe(true);
|
|
787
|
+
expect(wrapped.safeParse("a-x").success).toBe(false);
|
|
788
|
+
expect(wrapped.safeParse("a-wxyz").success).toBe(false);
|
|
789
|
+
|
|
790
|
+
const union = z.templateLiteral(["", z.union([z.string().regex(/^[a-c]+$/), z.number().int()])]);
|
|
791
|
+
expect(union.safeParse("abc").success).toBe(true);
|
|
792
|
+
expect(union.safeParse("12").success).toBe(true);
|
|
793
|
+
expect(union.safeParse("xyz").success).toBe(false);
|
|
794
|
+
expect(union.safeParse("1.5").success).toBe(false);
|
|
795
|
+
|
|
796
|
+
// an empty length range matches nothing rather than building an invalid quantifier
|
|
797
|
+
const empty = z.templateLiteral(["", z.string().min(8).length(5)]);
|
|
798
|
+
expect(empty.safeParse("abcde").success).toBe(false);
|
|
799
|
+
|
|
800
|
+
// lazy resolves its inner schema on the internals, not the def
|
|
801
|
+
const lazy = z.templateLiteral(["", z.lazy(() => z.string().min(2)).optional()]);
|
|
802
|
+
expect(lazy.safeParse("x").success).toBe(false);
|
|
803
|
+
expect(lazy.safeParse("xy").success).toBe(true);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("an embedded email does not constrain the other parts", () => {
|
|
807
|
+
const schema = z.templateLiteral([z.email(), "|", z.string()]);
|
|
808
|
+
expect(schema.safeParse("a@b.cc|a..b").success).toBe(true);
|
|
809
|
+
expect(schema.safeParse("a@b.cc|xy").success).toBe(true);
|
|
810
|
+
expect(schema.safeParse("a..b@b.cc|xy").success).toBe(false);
|
|
811
|
+
expect(schema.safeParse(".a@b.cc|xy").success).toBe(false);
|
|
812
|
+
});
|
|
@@ -79,7 +79,7 @@ describe("toJSONSchema", () => {
|
|
|
79
79
|
{
|
|
80
80
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
81
81
|
"format": "email",
|
|
82
|
-
"pattern": "^(
|
|
82
|
+
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
|
|
83
83
|
"type": "string",
|
|
84
84
|
}
|
|
85
85
|
`);
|
|
@@ -218,6 +218,15 @@ describe("toJSONSchema", () => {
|
|
|
218
218
|
"type": "string",
|
|
219
219
|
}
|
|
220
220
|
`);
|
|
221
|
+
expect(z.toJSONSchema(z.base64url())).toMatchInlineSnapshot(`
|
|
222
|
+
{
|
|
223
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
224
|
+
"contentEncoding": "base64url",
|
|
225
|
+
"format": "base64url",
|
|
226
|
+
"pattern": "^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2,3})?$",
|
|
227
|
+
"type": "string",
|
|
228
|
+
}
|
|
229
|
+
`);
|
|
221
230
|
expect(z.toJSONSchema(z.cuid())).toMatchInlineSnapshot(`
|
|
222
231
|
{
|
|
223
232
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -231,7 +240,7 @@ describe("toJSONSchema", () => {
|
|
|
231
240
|
{
|
|
232
241
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
233
242
|
"format": "emoji",
|
|
234
|
-
"pattern": "^[\\p{Extended_Pictographic}\\p{Emoji_Component}]+$",
|
|
243
|
+
"pattern": "^(?=[\\s\\S]*[\\p{Extended_Pictographic}\\p{Regional_Indicator}\\u20E3])[\\p{Extended_Pictographic}\\p{Emoji_Component}]+$",
|
|
235
244
|
"type": "string",
|
|
236
245
|
}
|
|
237
246
|
`);
|
|
@@ -356,7 +365,7 @@ describe("toJSONSchema", () => {
|
|
|
356
365
|
{
|
|
357
366
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
358
367
|
"format": "email",
|
|
359
|
-
"pattern": "^(
|
|
368
|
+
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
|
|
360
369
|
"type": "string",
|
|
361
370
|
}
|
|
362
371
|
`);
|
|
@@ -482,6 +491,30 @@ describe("toJSONSchema", () => {
|
|
|
482
491
|
`);
|
|
483
492
|
});
|
|
484
493
|
|
|
494
|
+
test("base64url pattern agrees with parse", () => {
|
|
495
|
+
const schema = z.base64url();
|
|
496
|
+
const pattern = new RegExp(z.toJSONSchema(schema).pattern!);
|
|
497
|
+
// lengths 1 (mod 4) carry no whole byte; the emitted pattern must reject them like parse does
|
|
498
|
+
for (const s of ["", "A", "AA", "AAA", "AAAA", "AAAAA"]) {
|
|
499
|
+
expect(pattern.test(s)).toBe(schema.safeParse(s).success);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("base64 pattern agrees with parse", () => {
|
|
504
|
+
const schema = z.base64();
|
|
505
|
+
const pattern = new RegExp(z.toJSONSchema(schema).pattern!);
|
|
506
|
+
// the runtime def.pattern is lax; the emitted pattern must still carry the block structure parse enforces
|
|
507
|
+
for (const s of ["", "A", "AA", "AAA", "AAAA", "AAAAA", "AA==", "AAA=", "A===", "=", "AAAA=="]) {
|
|
508
|
+
expect(pattern.test(s)).toBe(schema.safeParse(s).success);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("looseRecord with a format key emits the exact pattern", () => {
|
|
513
|
+
// recordProcessor reads bag.patterns directly; it must apply the same lax-to-exact swap as stringProcessor
|
|
514
|
+
const json = z.toJSONSchema(z.looseRecord(z.base64url(), z.number()));
|
|
515
|
+
expect(Object.keys(json.patternProperties!)).toEqual([z.regexes.base64url.source]);
|
|
516
|
+
});
|
|
517
|
+
|
|
485
518
|
test("string patterns", () => {
|
|
486
519
|
expect(
|
|
487
520
|
z.toJSONSchema(
|
|
@@ -724,6 +757,117 @@ describe("toJSONSchema", () => {
|
|
|
724
757
|
`);
|
|
725
758
|
});
|
|
726
759
|
|
|
760
|
+
test("number constraints order independence with int()", () => {
|
|
761
|
+
// .int() applied last should not overwrite the tighter bounds .min()/.max() already stored
|
|
762
|
+
expect(z.toJSONSchema(z.number().int().min(0).max(23))).toMatchInlineSnapshot(`
|
|
763
|
+
{
|
|
764
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
765
|
+
"maximum": 23,
|
|
766
|
+
"minimum": 0,
|
|
767
|
+
"type": "integer",
|
|
768
|
+
}
|
|
769
|
+
`);
|
|
770
|
+
expect(z.toJSONSchema(z.number().min(0).max(23).int())).toMatchInlineSnapshot(`
|
|
771
|
+
{
|
|
772
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
773
|
+
"maximum": 23,
|
|
774
|
+
"minimum": 0,
|
|
775
|
+
"type": "integer",
|
|
776
|
+
}
|
|
777
|
+
`);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("chained multipleOf divisors emit the whole conjunction", () => {
|
|
781
|
+
// runtime rejects 4 (not a multiple of 3), so the emitted schema must too
|
|
782
|
+
expect(z.toJSONSchema(z.number().multipleOf(2).multipleOf(3))).toMatchInlineSnapshot(`
|
|
783
|
+
{
|
|
784
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
785
|
+
"allOf": [
|
|
786
|
+
{
|
|
787
|
+
"multipleOf": 3,
|
|
788
|
+
},
|
|
789
|
+
],
|
|
790
|
+
"multipleOf": 2,
|
|
791
|
+
"type": "number",
|
|
792
|
+
}
|
|
793
|
+
`);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("length() does not overwrite a tighter earlier bound", () => {
|
|
797
|
+
// min(8) + length(5) matches nothing at runtime; the emitted bounds stay honest about that
|
|
798
|
+
expect(z.toJSONSchema(z.string().min(8).length(5))).toMatchInlineSnapshot(`
|
|
799
|
+
{
|
|
800
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
801
|
+
"maxLength": 5,
|
|
802
|
+
"minLength": 8,
|
|
803
|
+
"type": "string",
|
|
804
|
+
}
|
|
805
|
+
`);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test("an integer format keeps the emitted type integer whatever format lands last", () => {
|
|
809
|
+
// runtime rejects 1.5 in both orders, so both must emit type integer
|
|
810
|
+
const after = z.number().int().check(z.float32());
|
|
811
|
+
expect(after.safeParse(1.5).success).toBe(false);
|
|
812
|
+
expect(z.toJSONSchema(after).type).toBe("integer");
|
|
813
|
+
expect(z.toJSONSchema(z.number().check(z.float32()).int()).type).toBe("integer");
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test("disjoint mime checks emit a false schema, not an unconstrained file", () => {
|
|
817
|
+
const file = z.file().mime("image/png").mime("text/plain");
|
|
818
|
+
expect(z.toJSONSchema(file)).toMatchInlineSnapshot(`
|
|
819
|
+
{
|
|
820
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
821
|
+
"contentEncoding": "binary",
|
|
822
|
+
"format": "binary",
|
|
823
|
+
"not": {},
|
|
824
|
+
"type": "string",
|
|
825
|
+
}
|
|
826
|
+
`);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
test("a custom mime narrowing via onattach intersects with built-in mime checks", () => {
|
|
830
|
+
const check = {
|
|
831
|
+
_zod: {
|
|
832
|
+
def: { check: "custom_mime" },
|
|
833
|
+
onattach: [
|
|
834
|
+
(inst: any) => {
|
|
835
|
+
inst._zod.bag.mime = ["text/plain"];
|
|
836
|
+
},
|
|
837
|
+
],
|
|
838
|
+
check: () => {},
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
const file = z
|
|
842
|
+
.file()
|
|
843
|
+
.mime(["image/png", "text/plain"])
|
|
844
|
+
.check(check as any);
|
|
845
|
+
expect(z.toJSONSchema(file).contentMediaType).toBe("text/plain");
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test("a custom check contributing via onattach still lands", () => {
|
|
849
|
+
// third-party checks have no converter handler; their bag writes are merged in as a fallback
|
|
850
|
+
const check = {
|
|
851
|
+
_zod: {
|
|
852
|
+
def: { check: "custom_range" },
|
|
853
|
+
onattach: [
|
|
854
|
+
(inst: any) => {
|
|
855
|
+
const bag = inst._zod.bag;
|
|
856
|
+
if (bag.minimum === undefined || 5 > bag.minimum) bag.minimum = 5;
|
|
857
|
+
},
|
|
858
|
+
],
|
|
859
|
+
check: () => {},
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
expect(z.toJSONSchema(z.number().check(check as any))).toMatchInlineSnapshot(`
|
|
863
|
+
{
|
|
864
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
865
|
+
"minimum": 5,
|
|
866
|
+
"type": "number",
|
|
867
|
+
}
|
|
868
|
+
`);
|
|
869
|
+
});
|
|
870
|
+
|
|
727
871
|
test("target normalization draft-04 and draft-07", () => {
|
|
728
872
|
// Test that both old (draft-4, draft-7) and new (draft-04, draft-07) target formats work
|
|
729
873
|
|