zod 4.6.3 → 4.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/package.json +1 -1
  2. package/src/v4/classic/schemas.ts +9 -4
  3. package/src/v4/classic/tests/assignability.test.ts +1 -0
  4. package/src/v4/classic/tests/instance-footprint.test.ts +22 -0
  5. package/src/v4/classic/tests/number.test.ts +39 -0
  6. package/src/v4/classic/tests/string.test.ts +70 -1
  7. package/src/v4/classic/tests/to-json-schema.test.ts +87 -35
  8. package/src/v4/core/api.ts +17 -22
  9. package/src/v4/core/compile.ts +27 -6
  10. package/src/v4/core/core.ts +1 -3
  11. package/src/v4/core/json-schema-processors.ts +9 -13
  12. package/src/v4/core/regexes.ts +4 -0
  13. package/src/v4/core/schemas.ts +35 -11
  14. package/src/v4/core/tests/compile-differential.test.ts +35 -0
  15. package/src/v4/core/tests/compile.test.ts +177 -0
  16. package/src/v4/core/tests/url-no-canparse.test.ts +118 -16
  17. package/src/v4/core/versions.ts +1 -1
  18. package/src/v4/locales/ar.ts +1 -0
  19. package/src/v4/locales/az.ts +1 -0
  20. package/src/v4/locales/be.ts +1 -0
  21. package/src/v4/locales/bg.ts +1 -0
  22. package/src/v4/locales/bn.ts +1 -0
  23. package/src/v4/locales/ca.ts +1 -0
  24. package/src/v4/locales/ckb.ts +1 -0
  25. package/src/v4/locales/cs.ts +1 -0
  26. package/src/v4/locales/da.ts +1 -0
  27. package/src/v4/locales/de.ts +1 -0
  28. package/src/v4/locales/el.ts +1 -0
  29. package/src/v4/locales/en.ts +1 -0
  30. package/src/v4/locales/eo.ts +1 -0
  31. package/src/v4/locales/es.ts +1 -0
  32. package/src/v4/locales/fa.ts +1 -0
  33. package/src/v4/locales/fi.ts +1 -0
  34. package/src/v4/locales/fr-CA.ts +1 -0
  35. package/src/v4/locales/fr.ts +1 -0
  36. package/src/v4/locales/gu.ts +1 -0
  37. package/src/v4/locales/he.ts +1 -0
  38. package/src/v4/locales/hi.ts +1 -0
  39. package/src/v4/locales/hr.ts +1 -0
  40. package/src/v4/locales/hu.ts +1 -0
  41. package/src/v4/locales/hy.ts +1 -0
  42. package/src/v4/locales/id.ts +1 -0
  43. package/src/v4/locales/is.ts +1 -0
  44. package/src/v4/locales/it.ts +1 -0
  45. package/src/v4/locales/ja.ts +1 -0
  46. package/src/v4/locales/ka.ts +1 -0
  47. package/src/v4/locales/km.ts +1 -0
  48. package/src/v4/locales/kn.ts +1 -0
  49. package/src/v4/locales/ko.ts +1 -0
  50. package/src/v4/locales/lt.ts +1 -0
  51. package/src/v4/locales/mk.ts +1 -0
  52. package/src/v4/locales/ms.ts +1 -0
  53. package/src/v4/locales/ne.ts +1 -0
  54. package/src/v4/locales/nl.ts +1 -0
  55. package/src/v4/locales/nn.ts +1 -0
  56. package/src/v4/locales/no.ts +1 -0
  57. package/src/v4/locales/ota.ts +1 -0
  58. package/src/v4/locales/pl.ts +1 -0
  59. package/src/v4/locales/ps.ts +1 -0
  60. package/src/v4/locales/pt-BR.ts +1 -0
  61. package/src/v4/locales/pt.ts +1 -0
  62. package/src/v4/locales/ro.ts +1 -0
  63. package/src/v4/locales/ru.ts +1 -0
  64. package/src/v4/locales/sk.ts +1 -0
  65. package/src/v4/locales/sl.ts +1 -0
  66. package/src/v4/locales/sv.ts +1 -0
  67. package/src/v4/locales/ta.ts +1 -0
  68. package/src/v4/locales/tg.ts +1 -0
  69. package/src/v4/locales/th.ts +1 -0
  70. package/src/v4/locales/tk.ts +1 -0
  71. package/src/v4/locales/tr.ts +1 -0
  72. package/src/v4/locales/uk.ts +1 -0
  73. package/src/v4/locales/ur.ts +1 -0
  74. package/src/v4/locales/uz.ts +1 -0
  75. package/src/v4/locales/vi.ts +1 -0
  76. package/src/v4/locales/yo.ts +1 -0
  77. package/src/v4/locales/zh-CN.ts +1 -0
  78. package/src/v4/locales/zh-TW.ts +1 -0
  79. package/src/v4/mini/schemas.ts +12 -4
  80. package/src/v4/mini/tests/index.test.ts +56 -0
  81. package/src/v4/mini/tests/string.test.ts +7 -0
  82. package/v4/classic/schemas.cjs +9 -4
  83. package/v4/classic/schemas.d.cts +1 -0
  84. package/v4/classic/schemas.d.ts +1 -0
  85. package/v4/classic/schemas.js +8 -4
  86. package/v4/core/api.cjs +9 -20
  87. package/v4/core/api.d.cts +6 -2
  88. package/v4/core/api.d.ts +6 -2
  89. package/v4/core/api.js +9 -20
  90. package/v4/core/compile.cjs +7 -6
  91. package/v4/core/compile.js +8 -7
  92. package/v4/core/core.cjs +1 -1
  93. package/v4/core/core.js +1 -1
  94. package/v4/core/json-schema-processors.cjs +7 -10
  95. package/v4/core/json-schema-processors.js +7 -10
  96. package/v4/core/regexes.cjs +4 -2
  97. package/v4/core/regexes.d.cts +1 -0
  98. package/v4/core/regexes.d.ts +1 -0
  99. package/v4/core/regexes.js +2 -0
  100. package/v4/core/schemas.cjs +32 -11
  101. package/v4/core/schemas.d.cts +5 -3
  102. package/v4/core/schemas.d.ts +5 -3
  103. package/v4/core/schemas.js +30 -11
  104. package/v4/core/versions.cjs +1 -1
  105. package/v4/core/versions.js +1 -1
  106. package/v4/locales/ar.cjs +1 -0
  107. package/v4/locales/ar.js +1 -0
  108. package/v4/locales/az.cjs +1 -0
  109. package/v4/locales/az.js +1 -0
  110. package/v4/locales/be.cjs +1 -0
  111. package/v4/locales/be.js +1 -0
  112. package/v4/locales/bg.cjs +1 -0
  113. package/v4/locales/bg.js +1 -0
  114. package/v4/locales/bn.cjs +1 -0
  115. package/v4/locales/bn.js +1 -0
  116. package/v4/locales/ca.cjs +1 -0
  117. package/v4/locales/ca.js +1 -0
  118. package/v4/locales/ckb.cjs +1 -0
  119. package/v4/locales/ckb.js +1 -0
  120. package/v4/locales/cs.cjs +1 -0
  121. package/v4/locales/cs.js +1 -0
  122. package/v4/locales/da.cjs +1 -0
  123. package/v4/locales/da.js +1 -0
  124. package/v4/locales/de.cjs +1 -0
  125. package/v4/locales/de.js +1 -0
  126. package/v4/locales/el.cjs +1 -0
  127. package/v4/locales/el.js +1 -0
  128. package/v4/locales/en.cjs +1 -0
  129. package/v4/locales/en.js +1 -0
  130. package/v4/locales/eo.cjs +1 -0
  131. package/v4/locales/eo.js +1 -0
  132. package/v4/locales/es.cjs +1 -0
  133. package/v4/locales/es.js +1 -0
  134. package/v4/locales/fa.cjs +1 -0
  135. package/v4/locales/fa.js +1 -0
  136. package/v4/locales/fi.cjs +1 -0
  137. package/v4/locales/fi.js +1 -0
  138. package/v4/locales/fr-CA.cjs +1 -0
  139. package/v4/locales/fr-CA.js +1 -0
  140. package/v4/locales/fr.cjs +1 -0
  141. package/v4/locales/fr.js +1 -0
  142. package/v4/locales/gu.cjs +1 -0
  143. package/v4/locales/gu.js +1 -0
  144. package/v4/locales/he.cjs +1 -0
  145. package/v4/locales/he.js +1 -0
  146. package/v4/locales/hi.cjs +1 -0
  147. package/v4/locales/hi.js +1 -0
  148. package/v4/locales/hr.cjs +1 -0
  149. package/v4/locales/hr.js +1 -0
  150. package/v4/locales/hu.cjs +1 -0
  151. package/v4/locales/hu.js +1 -0
  152. package/v4/locales/hy.cjs +1 -0
  153. package/v4/locales/hy.js +1 -0
  154. package/v4/locales/id.cjs +1 -0
  155. package/v4/locales/id.js +1 -0
  156. package/v4/locales/is.cjs +1 -0
  157. package/v4/locales/is.js +1 -0
  158. package/v4/locales/it.cjs +1 -0
  159. package/v4/locales/it.js +1 -0
  160. package/v4/locales/ja.cjs +1 -0
  161. package/v4/locales/ja.js +1 -0
  162. package/v4/locales/ka.cjs +1 -0
  163. package/v4/locales/ka.js +1 -0
  164. package/v4/locales/km.cjs +1 -0
  165. package/v4/locales/km.js +1 -0
  166. package/v4/locales/kn.cjs +1 -0
  167. package/v4/locales/kn.js +1 -0
  168. package/v4/locales/ko.cjs +1 -0
  169. package/v4/locales/ko.js +1 -0
  170. package/v4/locales/lt.cjs +1 -0
  171. package/v4/locales/lt.js +1 -0
  172. package/v4/locales/mk.cjs +1 -0
  173. package/v4/locales/mk.js +1 -0
  174. package/v4/locales/ms.cjs +1 -0
  175. package/v4/locales/ms.js +1 -0
  176. package/v4/locales/ne.cjs +1 -0
  177. package/v4/locales/ne.js +1 -0
  178. package/v4/locales/nl.cjs +1 -0
  179. package/v4/locales/nl.js +1 -0
  180. package/v4/locales/nn.cjs +1 -0
  181. package/v4/locales/nn.js +1 -0
  182. package/v4/locales/no.cjs +1 -0
  183. package/v4/locales/no.js +1 -0
  184. package/v4/locales/ota.cjs +1 -0
  185. package/v4/locales/ota.js +1 -0
  186. package/v4/locales/pl.cjs +1 -0
  187. package/v4/locales/pl.js +1 -0
  188. package/v4/locales/ps.cjs +1 -0
  189. package/v4/locales/ps.js +1 -0
  190. package/v4/locales/pt-BR.cjs +1 -0
  191. package/v4/locales/pt-BR.js +1 -0
  192. package/v4/locales/pt.cjs +1 -0
  193. package/v4/locales/pt.js +1 -0
  194. package/v4/locales/ro.cjs +1 -0
  195. package/v4/locales/ro.js +1 -0
  196. package/v4/locales/ru.cjs +1 -0
  197. package/v4/locales/ru.js +1 -0
  198. package/v4/locales/sk.cjs +1 -0
  199. package/v4/locales/sk.js +1 -0
  200. package/v4/locales/sl.cjs +1 -0
  201. package/v4/locales/sl.js +1 -0
  202. package/v4/locales/sv.cjs +1 -0
  203. package/v4/locales/sv.js +1 -0
  204. package/v4/locales/ta.cjs +1 -0
  205. package/v4/locales/ta.js +1 -0
  206. package/v4/locales/tg.cjs +1 -0
  207. package/v4/locales/tg.js +1 -0
  208. package/v4/locales/th.cjs +1 -0
  209. package/v4/locales/th.js +1 -0
  210. package/v4/locales/tk.cjs +1 -0
  211. package/v4/locales/tk.js +1 -0
  212. package/v4/locales/tr.cjs +1 -0
  213. package/v4/locales/tr.js +1 -0
  214. package/v4/locales/uk.cjs +1 -0
  215. package/v4/locales/uk.js +1 -0
  216. package/v4/locales/ur.cjs +1 -0
  217. package/v4/locales/ur.js +1 -0
  218. package/v4/locales/uz.cjs +1 -0
  219. package/v4/locales/uz.js +1 -0
  220. package/v4/locales/vi.cjs +1 -0
  221. package/v4/locales/vi.js +1 -0
  222. package/v4/locales/yo.cjs +1 -0
  223. package/v4/locales/yo.js +1 -0
  224. package/v4/locales/zh-CN.cjs +1 -0
  225. package/v4/locales/zh-CN.js +1 -0
  226. package/v4/locales/zh-TW.cjs +1 -0
  227. package/v4/locales/zh-TW.js +1 -0
  228. package/v4/mini/schemas.cjs +10 -4
  229. package/v4/mini/schemas.d.cts +1 -0
  230. package/v4/mini/schemas.d.ts +1 -0
  231. package/v4/mini/schemas.js +9 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod",
3
- "version": "4.6.3",
3
+ "version": "4.6.4",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -2,6 +2,7 @@ import type { $ZodBigIntFormats } from "../core/checks.js";
2
2
  import * as core from "../core/index.js";
3
3
  import { util, type $ZodNumberFormats } from "../core/index.js";
4
4
  import * as processors from "../core/json-schema-processors.js";
5
+ import * as regexes from "../core/regexes.js";
5
6
  import type { StandardSchemaWithJSONProps } from "../core/standard-schema.js";
6
7
  import { createStandardJSONSchemaMethod, createToJSONSchemaMethod } from "../core/to-json-schema.js";
7
8
  import en from "../locales/en.js";
@@ -770,8 +771,8 @@ export function url(params?: string | core.$ZodURLParams): ZodURL {
770
771
 
771
772
  export function httpUrl(params?: string | Omit<core.$ZodURLParams, "protocol" | "hostname">): ZodURL {
772
773
  return core._url(ZodURL, {
773
- protocol: core.regexes.httpProtocol,
774
- hostname: core.regexes.domain,
774
+ protocol: regexes.httpProtocol,
775
+ hostname: regexes.domain,
775
776
  ...util.normalizeParams(params),
776
777
  });
777
778
  }
@@ -1081,11 +1082,15 @@ export function stringFormat<Format extends string>(
1081
1082
  }
1082
1083
 
1083
1084
  export function hostname(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"hostname"> {
1084
- return core._stringFormat(ZodCustomStringFormat, "hostname", core.regexes.hostname, _params) as any;
1085
+ return core._stringFormat(ZodCustomStringFormat, "hostname", regexes.hostname, _params) as any;
1085
1086
  }
1086
1087
 
1087
1088
  export function hex(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"hex"> {
1088
- return core._stringFormat(ZodCustomStringFormat, "hex", core.regexes.hex, _params) as any;
1089
+ return core._stringFormat(ZodCustomStringFormat, "hex", regexes.hex, _params) as any;
1090
+ }
1091
+
1092
+ export function currencyCode(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"currency_code"> {
1093
+ return core._stringFormat(ZodCustomStringFormat, "currency_code", regexes.currencyCode, _params) as any;
1089
1094
  }
1090
1095
 
1091
1096
  export function hash<Alg extends util.HashAlgorithm, Enc extends util.HashEncoding = "hex">(
@@ -352,6 +352,7 @@ test("checks", () => {
352
352
  const _b: z.core.$ZodCheck = {} as any as z.core.$ZodStringFormatChecks;
353
353
  const _c: z.core.$ZodType = {} as any as z.core.$ZodTypes;
354
354
  const _d: z.core.$ZodType = {} as any as z.core.$ZodStringFormatTypes;
355
+ z.currencyCode() satisfies z.core.$ZodStringFormatTypes;
355
356
  });
356
357
 
357
358
  test("assignability to $ZodType", () => {
@@ -105,6 +105,28 @@ test("deferred initializers are released after construction", () => {
105
105
  expect(z.object({ a: z.string() })._zod.deferred).toEqual(undefined);
106
106
  });
107
107
 
108
+ test("trait initializers run once per instance across repeated entry", () => {
109
+ const calls: string[] = [];
110
+ const Base: core.$constructor<any> = core.$constructor<any>("CountedBase", () => calls.push("base"));
111
+ const Child: core.$constructor<any> = core.$constructor<any>("CountedChild", (inst, def) => {
112
+ calls.push("child");
113
+ Base.init(inst, def);
114
+ Base.init(inst, def);
115
+ });
116
+ const constructed = new Child({});
117
+ Child.init(constructed, {});
118
+ const direct = Object.create({});
119
+ Child.init(direct, {});
120
+ Child.init(direct, {});
121
+ expect(calls).toEqual(["child", "base", "child", "base"]);
122
+ for (const inst of [constructed, direct]) {
123
+ expect([...inst._zod.traits]).toEqual(["CountedChild", "CountedBase"]);
124
+ expect(inst instanceof Base).toBe(true);
125
+ expect(inst instanceof Child).toBe(true);
126
+ }
127
+ expect(constructed._zod.traits).not.toBe(direct._zod.traits);
128
+ });
129
+
108
130
  test("a trait initializer called directly still installs its members", () => {
109
131
  // a direct `init` installs onto the receiver's own prototype, since it is not below the constructor's
110
132
  z.string();
@@ -2,6 +2,45 @@ import { expect, expectTypeOf, test } from "vitest";
2
2
 
3
3
  import * as z from "zod/v4";
4
4
 
5
+ test("number factory checks", () => {
6
+ const schema = z.number({ checks: [z.gte(1), z.lte(3)] as const });
7
+ const chained = z.number().min(1).max(3);
8
+ expectTypeOf<z.output<typeof schema>>().toEqualTypeOf<number>();
9
+ for (const value of [0, 1, 3, 4, Number.NaN, "2", undefined]) {
10
+ expect(z.validate(schema, value)).toBe(z.validate(chained, value));
11
+ expect(schema.safeParse(value)).toEqual(chained.safeParse(value));
12
+ }
13
+ expect(z.toJSONSchema(schema)).toEqual(z.toJSONSchema(chained));
14
+ expect(schema.minValue).toBe(1);
15
+ expect(schema.maxValue).toBe(3);
16
+ expect(z.coerce.number({ checks: [z.gte(1)] }).parse("2")).toBe(2);
17
+ const overwritten = z.number({ checks: [z.overwrite<number>((value) => value + 1), z.gte(2)] });
18
+ expect(overwritten.parse(1)).toBe(2);
19
+ expect(z.validate(overwritten, 0)).toBe(false);
20
+ // @ts-expect-error length checks cannot validate numbers
21
+ z.number({ checks: [z.minLength(1)] });
22
+ });
23
+
24
+ test("number factory checks snapshot caller arrays", () => {
25
+ for (const factory of [z.number, z.coerce.number]) {
26
+ const checks = [z.gte(1)];
27
+ const schema = factory({ checks });
28
+ let reads = 0;
29
+ factory({
30
+ get checks() {
31
+ if (++reads > 1) throw new Error("checks read twice");
32
+ return checks;
33
+ },
34
+ });
35
+ expect(reads).toBe(1);
36
+ const before = z.toJSONSchema(schema);
37
+ checks.push(z.gte(10));
38
+ expect(z.validate(schema, 3)).toBe(true);
39
+ expect(z.validate(z.compile(schema), 3)).toBe(true);
40
+ expect(z.toJSONSchema(schema)).toEqual(before);
41
+ }
42
+ });
43
+
5
44
  test("z.number() basic validation", () => {
6
45
  const schema = z.number();
7
46
  expect(schema.parse(1234)).toEqual(1234);
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { expect, test } from "vitest";
2
+ import { expect, expectTypeOf, test } from "vitest";
3
3
 
4
4
  import * as z from "zod/v4";
5
5
 
@@ -12,6 +12,63 @@ const includesFromIndex2 = z.string().includes("includes", { position: 2 });
12
12
  const startsWith = z.string().startsWith("startsWith");
13
13
  const endsWith = z.string().endsWith("endsWith");
14
14
 
15
+ test("string factory checks", async () => {
16
+ const schema = z.string({ checks: [z.minLength(1), z.maxLength(3)] as const });
17
+ const chained = z.string().min(1).max(3);
18
+ expectTypeOf<z.output<typeof schema>>().toEqualTypeOf<string>();
19
+ for (const value of ["", "ab", "abcd", 1, undefined]) {
20
+ expect(z.validate(schema, value)).toBe(z.validate(chained, value));
21
+ expect(schema.safeParse(value)).toEqual(chained.safeParse(value));
22
+ }
23
+ expect(z.toJSONSchema(schema)).toEqual(z.toJSONSchema(chained));
24
+ expect(schema.minLength).toBe(1);
25
+ expect(schema.maxLength).toBe(3);
26
+ expect(schema._zod.parent).toBeUndefined();
27
+ expect([...schema._zod.traits]).toEqual([...chained._zod.traits]);
28
+ const extended = schema.max(2);
29
+ expect(extended._zod.parent).toBe(schema);
30
+ expect(z.validate(schema, "abc")).toBe(true);
31
+ expect(z.validate(extended, "abc")).toBe(false);
32
+ const coerce = z.coerce.string({ checks: [z.minLength(2)] });
33
+ expect(coerce.parse(12)).toBe("12");
34
+ expect(z.validate(coerce, 1)).toBe(false);
35
+ const asyncSchema = z.string({
36
+ checks: [
37
+ z.check<string>(async (ctx) => {
38
+ if (ctx.value !== "ok") ctx.issues.push({ code: "custom", input: ctx.value });
39
+ }),
40
+ ],
41
+ });
42
+ expect(await asyncSchema.safeParseAsync("ok")).toEqual({ success: true, data: "ok" });
43
+ expect((await asyncSchema.safeParseAsync("bad")).success).toBe(false);
44
+ // @ts-expect-error numeric checks cannot validate strings
45
+ z.string({ checks: [z.gte(1)] });
46
+ });
47
+
48
+ test("factory checks snapshot caller arrays", () => {
49
+ for (const factory of [z.string, z.coerce.string]) {
50
+ for (const initial of [[], [z.minLength(1)]]) {
51
+ const schema = factory({ checks: initial });
52
+ let reads = 0;
53
+ const getterSchema = factory({
54
+ get checks() {
55
+ if (++reads > 1) throw new Error("checks read twice");
56
+ return initial;
57
+ },
58
+ });
59
+ expect(reads).toBe(1);
60
+ expect(z.validate(getterSchema, "long")).toBe(true);
61
+ const before = z.toJSONSchema(schema);
62
+ initial.push(z.minLength(10));
63
+ expect(schema._zod.def.checks).not.toBe(initial);
64
+ expect(z.validate(schema, "long")).toBe(true);
65
+ expect(z.validate(z.compile(schema), "long")).toBe(true);
66
+ expect(z.toJSONSchema(schema)).toEqual(before);
67
+ expect(z.validate(schema.check(z.minLength(2)), "long")).toBe(true);
68
+ }
69
+ }
70
+ });
71
+
15
72
  test("length checks", () => {
16
73
  minFive.parse("12345");
17
74
  minFive.parse("123456");
@@ -1289,6 +1346,18 @@ test("hostname", () => {
1289
1346
  expect(() => hostname.parse("example..com")).toThrow();
1290
1347
  });
1291
1348
 
1349
+ test("currency code", () => {
1350
+ const currency = z.currencyCode();
1351
+ for (const code of ["USD", "EUR", "XXX", "XCG", "XAD"]) expect(currency.parse(code)).toBe(code);
1352
+ for (const input of ["", "usd", "US", "USDX", " USD", "ANG", "BGN"])
1353
+ expect(currency.safeParse(input).success).toBe(false);
1354
+ expect(currency.safeParse("usd").error!.issues[0]).toMatchObject({
1355
+ code: "invalid_format",
1356
+ format: "currency_code",
1357
+ message: "Invalid currency code",
1358
+ });
1359
+ });
1360
+
1292
1361
  test("hash validation", () => {
1293
1362
  // MD5 tests
1294
1363
  const md5hex = z.hash("md5");
@@ -1,5 +1,5 @@
1
1
  import { Validator } from "@seriousme/openapi-schema-validator";
2
- import { describe, expect, test } from "vitest";
2
+ import { describe, expect, test, vi } from "vitest";
3
3
  import * as z from "zod";
4
4
  // import * as zCore from "zod/v4/core";
5
5
 
@@ -3235,11 +3235,31 @@ test("basic registry", () => {
3235
3235
  `);
3236
3236
  });
3237
3237
 
3238
- test("large registry converts in linear time", () => {
3239
- const count = 2000;
3240
- const convert = (withIntersection: boolean) => {
3238
+ function countMapScanWork<T>(convert: () => T): { result: T; work: number } {
3239
+ const entries = Map.prototype.entries;
3240
+ const values = Map.prototype.values;
3241
+ let work = 0;
3242
+ const entriesSpy = vi.spyOn(Map.prototype, "entries").mockImplementation(function (this: Map<unknown, unknown>) {
3243
+ work += this.size;
3244
+ return entries.call(this);
3245
+ } as typeof Map.prototype.entries);
3246
+ const valuesSpy = vi.spyOn(Map.prototype, "values").mockImplementation(function (this: Map<unknown, unknown>) {
3247
+ work += this.size;
3248
+ return values.call(this);
3249
+ } as typeof Map.prototype.values);
3250
+ try {
3251
+ return { result: convert(), work };
3252
+ } finally {
3253
+ entriesSpy.mockRestore();
3254
+ valuesSpy.mockRestore();
3255
+ }
3256
+ }
3257
+
3258
+ test("large registry conversion performs linear map-scan work", () => {
3259
+ const count = 64;
3260
+ const convert = (size: number, withIntersection: boolean) => {
3241
3261
  const registry = z.registry<{ id: string }>();
3242
- for (let i = 0; i < count; i++) {
3262
+ for (let i = 0; i < size; i++) {
3243
3263
  registry.add(
3244
3264
  z.object({ id: z.string(), name: z.string(), count: z.number(), nested: z.object({ a: z.boolean() }) }),
3245
3265
  { id: `Type${i}` }
@@ -3247,55 +3267,53 @@ test("large registry converts in linear time", () => {
3247
3267
  }
3248
3268
  if (withIntersection) registry.add(z.object({ a: z.string() }).and(z.object({ b: z.string() })), { id: "Inter" });
3249
3269
 
3250
- const start = performance.now();
3251
- const { schemas } = z.toJSONSchema(registry, { uri: (id) => `https://example.com/${id}.json` });
3252
- return { schemas, elapsed: performance.now() - start };
3270
+ return countMapScanWork(() => z.toJSONSchema(registry, { uri: (id) => `https://example.com/${id}.json` }));
3253
3271
  };
3254
3272
 
3255
- const plain = convert(false);
3256
- expect(Object.keys(plain.schemas)).toHaveLength(count);
3257
- expect(plain.schemas.Type0).toMatchObject({
3273
+ const plain = convert(count, false);
3274
+ const plainDouble = convert(count * 2, false);
3275
+ expect(Object.keys(plain.result.schemas)).toHaveLength(count);
3276
+ expect(plain.result.schemas.Type0).toMatchObject({
3258
3277
  $id: "https://example.com/Type0.json",
3259
3278
  type: "object",
3260
3279
  properties: { nested: { type: "object" } },
3261
3280
  });
3262
- expect(plain.schemas[`Type${count - 1}`]!.$id).toBe(`https://example.com/Type${count - 1}.json`);
3263
-
3264
- // The whole-map passes in extractDefs/finalize used to re-run once per registered schema, which made this quadratic: ~9s of CPU at this size before the passes were hoisted, ~50ms after.
3265
- expect(plain.elapsed).toBeLessThan(5000);
3281
+ expect(plain.result.schemas[`Type${count - 1}`]!.$id).toBe(`https://example.com/Type${count - 1}.json`);
3282
+ expect(plain.work).toBeGreaterThan(0);
3283
+ expect(plainDouble.work).toBeLessThanOrEqual(plain.work * 2);
3266
3284
 
3267
- // The intersection fold walks the whole map as well, so it has to run inside the same guard. Hoisting it back out costs ~10x at this size. Comparing the two conversions rather than asserting a fixed budget keeps this independent of how fast the machine is.
3268
- const folded = convert(true);
3269
- expect(folded.schemas.Inter).toMatchObject({ type: "object", properties: { a: {}, b: {} } });
3270
- expect(folded.elapsed).toBeLessThan(plain.elapsed * 4 + 100);
3285
+ const folded = convert(count, true);
3286
+ const foldedDouble = convert(count * 2, true);
3287
+ expect(folded.result.schemas.Inter).toMatchObject({ type: "object", properties: { a: {}, b: {} } });
3288
+ expect(folded.work).toBeGreaterThan(0);
3289
+ expect(foldedDouble.work).toBeLessThanOrEqual(folded.work * 2);
3271
3290
  });
3272
3291
 
3273
- test("a registry of records with numeric keys converts in linear time", () => {
3274
- const count = 2000;
3275
- const convert = (key: (i: number) => z.core.$ZodType) => {
3292
+ test("a registry of records with numeric keys performs linear map-scan work", () => {
3293
+ const count = 64;
3294
+ const convert = (size: number, key: (i: number) => z.core.$ZodType) => {
3276
3295
  const registry = z.registry<{ id: string }>();
3277
- for (let i = 0; i < count; i++) {
3296
+ for (let i = 0; i < size; i++) {
3278
3297
  registry.add(z.object({ m: z.record(key(i) as z.core.$ZodRecordKey, z.boolean()) }), { id: `Type${i}` });
3279
3298
  }
3280
- const start = performance.now();
3281
- const { schemas } = z.toJSONSchema(registry, { uri: (id) => `https://example.com/${id}.json` });
3282
- return { schemas, elapsed: performance.now() - start };
3299
+ return countMapScanWork(() => z.toJSONSchema(registry, { uri: (id) => `https://example.com/${id}.json` }));
3283
3300
  };
3284
3301
 
3285
- const string = convert(() => z.string());
3286
- const numeric = convert(() => z.number());
3287
- expect(numeric.schemas.Type0).toMatchObject({
3302
+ const numeric = convert(count, () => z.number());
3303
+ const numericDouble = convert(count * 2, () => z.number());
3304
+ expect(numeric.result.schemas.Type0).toMatchObject({
3288
3305
  properties: { m: { propertyNames: { type: "string", pattern: "^-?\\d+(?:\\.\\d+)?$" } } },
3289
3306
  });
3307
+ expect(numeric.work).toBeGreaterThan(0);
3308
+ expect(numericDouble.work).toBeLessThanOrEqual(numeric.work * 2);
3290
3309
 
3291
- // The rewrite has to find every carrier the flatten copied `propertyNames` onto, which means a pass over the whole seen map. Running that once per record rather than once per conversion cost ~10x at this size. A string key needs no rewrite at all, so comparing against it keeps this independent of how fast the machine is.
3292
- expect(numeric.elapsed).toBeLessThan(string.elapsed * 2 + 50);
3293
-
3294
- // An extracted key resolves through a map built once per conversion rather than a search per reference. There is no stable timing control for that — extraction has its own $defs cost, which swamps the difference — so this only pins the shape.
3295
- const extracted = convert((i) => z.number().meta({ id: `Key${i}` }));
3296
- expect(extracted.schemas.Type0).toMatchObject({
3310
+ const extracted = convert(count, (i) => z.number().meta({ id: `Key${i}` }));
3311
+ const extractedDouble = convert(count * 2, (i) => z.number().meta({ id: `Key${i}` }));
3312
+ expect(extracted.result.schemas.Type0).toMatchObject({
3297
3313
  properties: { m: { propertyNames: { type: "string", pattern: "^-?\\d+(?:\\.\\d+)?$" } } },
3298
3314
  });
3315
+ expect(extracted.work).toBeGreaterThan(0);
3316
+ expect(extractedDouble.work).toBeLessThanOrEqual(extracted.work * 2);
3299
3317
  });
3300
3318
 
3301
3319
  test("registry extracts unregistered subschemas into __shared", () => {
@@ -3849,6 +3867,31 @@ test("input JSON schema resolves requiredness past transform and catch wrappers"
3849
3867
  expect(required(z.object({ a: z.transform((v: unknown) => String(v)).pipe(z.string()) }))).toEqual(["a"]);
3850
3868
  });
3851
3869
 
3870
+ test("object required keys preserve shape order and direction", () => {
3871
+ const symbol = Symbol("symbol");
3872
+ const shape: z.ZodRawShape = {
3873
+ 10: z.string(),
3874
+ 2: z.string().optional(),
3875
+ plain: z.string(),
3876
+ optional: z.string().optional(),
3877
+ defaulted: z.string().default("x"),
3878
+ caught: z.string().catch("x"),
3879
+ [symbol]: z.string(),
3880
+ };
3881
+ Object.defineProperty(shape, "__proto__", { value: z.string(), enumerable: true });
3882
+ Object.defineProperty(shape, "hidden", { value: z.string(), enumerable: false });
3883
+
3884
+ const input = z.toJSONSchema(z.object(shape), { io: "input", unrepresentable: "any" });
3885
+ const output = z.toJSONSchema(z.object(shape), { io: "output", unrepresentable: "any" });
3886
+
3887
+ expect(Object.keys(input.properties!)).toEqual(["2", "10", "plain", "optional", "defaulted", "caught", "__proto__"]);
3888
+ expect(input.required).toEqual(["10", "plain", "caught", "__proto__"]);
3889
+ expect(output.required).toEqual(["10", "plain", "defaulted", "caught", "__proto__"]);
3890
+ expect(Object.prototype.hasOwnProperty.call(input.properties!, "__proto__")).toBe(true);
3891
+ expect(input.properties).not.toHaveProperty("hidden");
3892
+ expect(Object.getOwnPropertySymbols(input.properties!)).toEqual([]);
3893
+ });
3894
+
3852
3895
  test("strip output-side examples from input JSON schema for codec", () => {
3853
3896
  const codec = z
3854
3897
  .codec(z.string(), z.number(), { decode: (s) => Number(s), encode: (n) => String(n) })
@@ -4679,3 +4722,12 @@ describe("intersection folding declines", () => {
4679
4722
  expect(allOf(schema)).toHaveLength(3);
4680
4723
  });
4681
4724
  });
4725
+
4726
+ test("currency code", () => {
4727
+ expect(z.toJSONSchema(z.currencyCode())).toEqual({
4728
+ $schema: "https://json-schema.org/draft/2020-12/schema",
4729
+ type: "string",
4730
+ format: "currency_code",
4731
+ pattern: z.regexes.currencyCode.source,
4732
+ });
4733
+ });
@@ -58,16 +58,20 @@ export type CheckTypeParams<
58
58
  > = Params<T, NonNullable<T["_zod"]["isst"] | T["_zod"]["issc"]>, "type" | "checks" | "error" | "check" | AlsoOmit>;
59
59
 
60
60
  // String
61
- export type $ZodStringParams = TypeParams<schemas.$ZodString<string>, "coerce">;
61
+ export type $ZodStringParams = TypeParams<schemas.$ZodString<string>, "coerce"> & {
62
+ checks?: readonly checks.$ZodCheck<string>[];
63
+ };
64
+
65
+ function snapshotChecks<T extends { checks?: readonly checks.$ZodCheck<never>[] }>(def: T) {
66
+ if (def.checks) def.checks = [...def.checks];
67
+ return def as T & { checks?: NonNullable<T["checks"]>[number][] };
68
+ }
62
69
  // @__NO_SIDE_EFFECTS__
63
70
  export function _string<T extends schemas.$ZodString>(
64
71
  Class: util.SchemaClass<T>,
65
72
  params?: string | $ZodStringParams
66
73
  ): T {
67
- return new Class({
68
- type: "string",
69
- ...util.normalizeParams(params),
70
- });
74
+ return new Class(snapshotChecks({ type: "string" as const, ...util.normalizeParams(params) }));
71
75
  }
72
76
 
73
77
  // @__NO_SIDE_EFFECTS__
@@ -75,11 +79,7 @@ export function _coercedString<T extends schemas.$ZodString>(
75
79
  Class: util.SchemaClass<T>,
76
80
  params?: string | $ZodStringParams
77
81
  ): T {
78
- return new Class({
79
- type: "string",
80
- coerce: true,
81
- ...util.normalizeParams(params),
82
- });
82
+ return new Class(snapshotChecks({ type: "string" as const, coerce: true, ...util.normalizeParams(params) }));
83
83
  }
84
84
 
85
85
  export type $ZodStringFormatParams = CheckTypeParams<
@@ -608,7 +608,9 @@ export function _isoDuration<T extends schemas.$ZodISODuration>(
608
608
  }
609
609
 
610
610
  // Number
611
- export type $ZodNumberParams = TypeParams<schemas.$ZodNumber<number>, "coerce">;
611
+ export type $ZodNumberParams = TypeParams<schemas.$ZodNumber<number>, "coerce"> & {
612
+ checks?: readonly checks.$ZodCheck<number>[];
613
+ };
612
614
  export type $ZodNumberFormatParams = CheckTypeParams<schemas.$ZodNumberFormat, "format" | "coerce">;
613
615
  export type $ZodCheckNumberFormatParams = CheckParams<checks.$ZodCheckNumberFormat, "format" | "when">;
614
616
  // @__NO_SIDE_EFFECTS__
@@ -616,11 +618,7 @@ export function _number<T extends schemas.$ZodNumber>(
616
618
  Class: util.SchemaClass<T>,
617
619
  params?: string | $ZodNumberParams
618
620
  ): T {
619
- return new Class({
620
- type: "number",
621
- checks: [],
622
- ...util.normalizeParams(params),
623
- });
621
+ return new Class(snapshotChecks({ type: "number" as const, checks: [], ...util.normalizeParams(params) }));
624
622
  }
625
623
 
626
624
  // @__NO_SIDE_EFFECTS__
@@ -628,12 +626,9 @@ export function _coercedNumber<T extends schemas.$ZodNumber>(
628
626
  Class: util.SchemaClass<T>,
629
627
  params?: string | $ZodNumberParams
630
628
  ): T {
631
- return new Class({
632
- type: "number",
633
- coerce: true,
634
- checks: [],
635
- ...util.normalizeParams(params),
636
- });
629
+ return new Class(
630
+ snapshotChecks({ type: "number" as const, coerce: true, checks: [], ...util.normalizeParams(params) })
631
+ );
637
632
  }
638
633
 
639
634
  // @__NO_SIDE_EFFECTS__
@@ -13,10 +13,10 @@ import {
13
13
  isValidIPv6,
14
14
  isValidJWT,
15
15
  mergeValues,
16
- parseURLObject,
17
16
  stripTabAndNewline,
18
17
  urlHostnameOk,
19
18
  urlProtocolOk,
19
+ validateURL,
20
20
  } from "./schemas.js";
21
21
  import type { ParseContextInternal, ParsePayload, SomeType } from "./schemas.js";
22
22
  import * as util from "./util.js";
@@ -772,7 +772,21 @@ const PATTERN_IS_COMPLETE: Set<string> = new Set([
772
772
  ]);
773
773
 
774
774
  // Returns the accessor holding the (possibly normalized) value after the check — url/normalize formats produce a new value like overwrite does. Never assigns to the incoming accessor: it may be a `const` or a property expression on user input.
775
- function generateStringFormatCheck(doc: Doc, ctx: CompileContext, def: StringFormatDef, accessor: string): string {
775
+ function generateStringFormatCheck(doc: Doc, ctx: CompileContext, def: StringFormatDef, accessor: string): string;
776
+ function generateStringFormatCheck(
777
+ doc: Doc,
778
+ ctx: CompileContext,
779
+ def: StringFormatDef,
780
+ accessor: string,
781
+ needsValue: boolean
782
+ ): string | null;
783
+ function generateStringFormatCheck(
784
+ doc: Doc,
785
+ ctx: CompileContext,
786
+ def: StringFormatDef,
787
+ accessor: string,
788
+ needsValue = true
789
+ ): string | null {
776
790
  // Some string formats do runtime validation beyond their advertised pattern. For cheap pure utility checks, hoist the runtime function and call it so the fast path stays correct without cloning the utility logic into codegen.
777
791
  const fmt = def.format;
778
792
  if (fmt === "base64") {
@@ -820,7 +834,7 @@ function generateStringFormatCheck(doc: Doc, ctx: CompileContext, def: StringFor
820
834
  formatDef.protocol !== undefined
821
835
  ) {
822
836
  // Same three predicates the runtime calls, in the same order, so there is no second URL implementation to drift. Which options exist is known now, so the calls the runtime makes conditionally are emitted conditionally instead.
823
- const parseConst = addConstant(ctx, parseURLObject);
837
+ const parseConst = addConstant(ctx, validateURL);
824
838
  const defConst = addConstant(ctx, def);
825
839
  const trimVar = newVar(ctx);
826
840
  const urlVar = newVar(ctx);
@@ -835,6 +849,7 @@ function generateStringFormatCheck(doc: Doc, ctx: CompileContext, def: StringFor
835
849
  const protocolConst = addConstant(ctx, urlProtocolOk);
836
850
  doc.write(`if (!${protocolConst}(${urlVar}, ${defConst}.protocol)) return INVALID;`);
837
851
  }
852
+ if (!needsValue) return null;
838
853
  const outputVar = newVar(ctx);
839
854
  const outputExpr = formatDef.normalize ? `${urlVar}.href` : `${addConstant(ctx, stripTabAndNewline)}(${trimVar})`;
840
855
  doc.write(`const ${outputVar} = ${outputExpr};`);
@@ -961,7 +976,7 @@ function generateCheck(
961
976
 
962
977
  switch (type) {
963
978
  case "string":
964
- typeAccessor = generateStringCheck(doc, ctx, schema, accessor);
979
+ typeAccessor = generateStringCheck(doc, ctx, schema, accessor, buildsValue);
965
980
  break;
966
981
  case "number":
967
982
  typeAccessor = generateNumberCheck(doc, schema, accessor);
@@ -1092,13 +1107,19 @@ function generateCheck(
1092
1107
  return generateChecks(doc, ctx, schema, typeAccessor);
1093
1108
  }
1094
1109
 
1095
- function generateStringCheck(doc: Doc, ctx: CompileContext, schema: SomeType, accessor: string): string {
1110
+ function generateStringCheck(
1111
+ doc: Doc,
1112
+ ctx: CompileContext,
1113
+ schema: SomeType,
1114
+ accessor: string,
1115
+ needsValue = true
1116
+ ): string | null {
1096
1117
  doc.write(`if (typeof ${accessor} !== "string") return INVALID;`);
1097
1118
 
1098
1119
  // z.email() carries its format on the def, z.string().email() in def.checks; both route here so the format table has no second copy to drift from.
1099
1120
  const def = schema._zod.def as unknown as StringFormatDef & { format?: string };
1100
1121
  if (def.format === undefined) return accessor;
1101
- return generateStringFormatCheck(doc, ctx, def, accessor);
1122
+ return generateStringFormatCheck(doc, ctx, def, accessor, needsValue);
1102
1123
  }
1103
1124
 
1104
1125
  function generateNumberCheck(doc: Doc, schema: SomeType, accessor: string): string {
@@ -79,9 +79,7 @@ export /*@__NO_SIDE_EFFECTS__*/ function $constructor<T extends ZodTrait, D = T[
79
79
  // Cleared even on throw, so the shared descriptor never leaks one instance's internals into the next.
80
80
  _zodDesc.value = undefined;
81
81
  }
82
- }
83
-
84
- if (inst._zod.traits.has(name)) {
82
+ } else if (inst._zod.traits.has(name)) {
85
83
  return;
86
84
  }
87
85
 
@@ -449,20 +449,16 @@ export const objectProcessor: Processor<schemas.$ZodObject> = (schema, ctx, _jso
449
449
  }
450
450
 
451
451
  // required keys
452
- const allKeys = new Set(Object.keys(shape));
453
- const requiredKeys = new Set(
454
- [...allKeys].filter((key) => {
455
- const field = def.shape[key]!;
456
- if (ctx.io === "input") {
457
- return inputOptin(field) === undefined;
458
- } else {
459
- return field._zod.optout === undefined;
460
- }
461
- })
462
- );
452
+ const requiredKeys: string[] = [];
453
+ for (const key of Object.keys(shape)) {
454
+ const field = def.shape[key]!;
455
+ if (ctx.io === "input" ? inputOptin(field) === undefined : field._zod.optout === undefined) {
456
+ requiredKeys.push(key);
457
+ }
458
+ }
463
459
 
464
- if (requiredKeys.size > 0) {
465
- json.required = Array.from(requiredKeys);
460
+ if (requiredKeys.length > 0) {
461
+ json.required = requiredKeys;
466
462
  }
467
463
 
468
464
  // catchall
@@ -98,6 +98,10 @@ export const e164: RegExp = /^\+[1-9]\d{6,14}$/;
98
98
  // Credit card shape: 12–19 digits, optionally separated by single spaces or single hyphens. ISO/IEC 7812 caps the PAN at 19 digits; 12 is the shortest issued length (Maestro).
99
99
  export const creditCard: RegExp = /^\d(?:[ -]?\d){11,18}$/;
100
100
 
101
+ // ISO 4217 alpha codes from the SIX list, regenerated by scripts/update-iso-4217.ts
102
+ export const currencyCode: RegExp =
103
+ /^(?:AED|AFN|ALL|AMD|AOA|ARS|AUD|AWG|AZN|BAM|BBD|BDT|BHD|BIF|BMD|BND|BOB|BOV|BRL|BSD|BTN|BWP|BYN|BZD|CAD|CDF|CHE|CHF|CHW|CLF|CLP|CNY|COP|COU|CRC|CUP|CVE|CZK|DJF|DKK|DOP|DZD|EGP|ERN|ETB|EUR|FJD|FKP|GBP|GEL|GHS|GIP|GMD|GNF|GTQ|GYD|HKD|HNL|HTG|HUF|IDR|ILS|INR|IQD|IRR|ISK|JMD|JOD|JPY|KES|KGS|KHR|KMF|KPW|KRW|KWD|KYD|KZT|LAK|LBP|LKR|LRD|LSL|LYD|MAD|MDL|MGA|MKD|MMK|MNT|MOP|MRU|MUR|MVR|MWK|MXN|MXV|MYR|MZN|NAD|NGN|NIO|NOK|NPR|NZD|OMR|PAB|PEN|PGK|PHP|PKR|PLN|PYG|QAR|RON|RSD|RUB|RWF|SAR|SBD|SCR|SDG|SEK|SGD|SHP|SLE|SOS|SRD|SSP|STN|SVC|SYP|SZL|THB|TJS|TMT|TND|TOP|TRY|TTD|TWD|TZS|UAH|UGX|USD|USN|UYI|UYU|UYW|UZS|VED|VES|VND|VUV|WST|XAD|XAF|XAG|XAU|XBA|XBB|XBC|XBD|XCD|XCG|XDR|XOF|XPD|XPF|XPT|XSU|XTS|XUA|XXX|YER|ZAR|ZMW|ZWG)$/;
104
+
101
105
  // iban electronic format: 2-letter country, check digits 02-98 (the only values `98 - remainder` can produce), 11-30 bban characters
102
106
  export const iban: RegExp = /^[A-Z]{2}(?!00|01|99)\d{2}[A-Z0-9]{11,30}$/;
103
107