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.
Files changed (348) hide show
  1. package/compile.cjs +19 -0
  2. package/index.cjs +19 -0
  3. package/locales/index.cjs +19 -0
  4. package/mini/index.cjs +19 -0
  5. package/package.json +8 -7
  6. package/src/v3/helpers/errorUtil.ts +1 -1
  7. package/src/v4/classic/checks.ts +0 -1
  8. package/src/v4/classic/external.ts +2 -0
  9. package/src/v4/classic/from-json-schema.ts +248 -25
  10. package/src/v4/classic/schemas.ts +255 -164
  11. package/src/v4/classic/tests/assignability.test.ts +59 -9
  12. package/src/v4/classic/tests/bigint.test.ts +6 -0
  13. package/src/v4/classic/tests/continuability.test.ts +2 -2
  14. package/src/v4/classic/tests/cyclic-data.test.ts +316 -0
  15. package/src/v4/classic/tests/default.test.ts +46 -0
  16. package/src/v4/classic/tests/enum.test.ts +9 -0
  17. package/src/v4/classic/tests/error.test.ts +130 -1
  18. package/src/v4/classic/tests/firstparty.test.ts +4 -0
  19. package/src/v4/classic/tests/from-json-schema.test.ts +157 -0
  20. package/src/v4/classic/tests/iban.test.ts +81 -0
  21. package/src/v4/classic/tests/instance-footprint.test.ts +18 -2
  22. package/src/v4/classic/tests/instanceof.test.ts +27 -7
  23. package/src/v4/classic/tests/issue-schema.test.ts +88 -56
  24. package/src/v4/classic/tests/object.test.ts +56 -0
  25. package/src/v4/classic/tests/optin-ladder.test.ts +3 -2
  26. package/src/v4/classic/tests/properties.test.ts +225 -0
  27. package/src/v4/classic/tests/recursive-types.test.ts +1 -1
  28. package/src/v4/classic/tests/standard-schema.test.ts +19 -0
  29. package/src/v4/classic/tests/string.test.ts +25 -0
  30. package/src/v4/classic/tests/template-literal.test.ts +33 -1
  31. package/src/v4/classic/tests/to-json-schema.test.ts +147 -3
  32. package/src/v4/classic/tests/validate.test.ts +569 -0
  33. package/src/v4/core/api.ts +30 -6
  34. package/src/v4/core/checks.ts +1 -101
  35. package/src/v4/core/compile.ts +193 -81
  36. package/src/v4/core/doc.ts +6 -2
  37. package/src/v4/core/errors.ts +3 -6
  38. package/src/v4/core/json-schema-generator.ts +2 -2
  39. package/src/v4/core/json-schema-processors.ts +226 -68
  40. package/src/v4/core/memoizer.ts +162 -36
  41. package/src/v4/core/parse.ts +37 -16
  42. package/src/v4/core/regexes.ts +10 -4
  43. package/src/v4/core/schemas.ts +314 -83
  44. package/src/v4/core/tests/cached.test.ts +54 -0
  45. package/src/v4/core/tests/compile-generative.test.ts +445 -0
  46. package/src/v4/core/tests/compile.test.ts +222 -2
  47. package/src/v4/core/tests/email-regex.test.ts +140 -0
  48. package/src/v4/core/tests/polyfill-collision.test.ts +46 -0
  49. package/src/v4/core/to-json-schema.ts +10 -8
  50. package/src/v4/core/util.ts +222 -147
  51. package/src/v4/core/versions.ts +2 -2
  52. package/src/v4/core/visit.ts +11 -0
  53. package/src/v4/locales/ar.ts +1 -0
  54. package/src/v4/locales/az.ts +1 -0
  55. package/src/v4/locales/be.ts +1 -0
  56. package/src/v4/locales/bg.ts +1 -0
  57. package/src/v4/locales/bn.ts +1 -0
  58. package/src/v4/locales/ca.ts +1 -0
  59. package/src/v4/locales/ckb.ts +1 -0
  60. package/src/v4/locales/cs.ts +1 -0
  61. package/src/v4/locales/da.ts +1 -0
  62. package/src/v4/locales/de.ts +1 -0
  63. package/src/v4/locales/el.ts +1 -0
  64. package/src/v4/locales/en.ts +1 -0
  65. package/src/v4/locales/eo.ts +1 -0
  66. package/src/v4/locales/es.ts +1 -0
  67. package/src/v4/locales/fa.ts +1 -0
  68. package/src/v4/locales/fi.ts +1 -0
  69. package/src/v4/locales/fr-CA.ts +1 -0
  70. package/src/v4/locales/fr.ts +1 -0
  71. package/src/v4/locales/gu.ts +1 -0
  72. package/src/v4/locales/he.ts +1 -0
  73. package/src/v4/locales/hi.ts +1 -0
  74. package/src/v4/locales/hr.ts +1 -0
  75. package/src/v4/locales/hu.ts +1 -0
  76. package/src/v4/locales/hy.ts +1 -0
  77. package/src/v4/locales/id.ts +1 -0
  78. package/src/v4/locales/is.ts +1 -0
  79. package/src/v4/locales/it.ts +1 -0
  80. package/src/v4/locales/ja.ts +1 -0
  81. package/src/v4/locales/ka.ts +1 -0
  82. package/src/v4/locales/km.ts +1 -0
  83. package/src/v4/locales/kn.ts +1 -0
  84. package/src/v4/locales/ko.ts +1 -0
  85. package/src/v4/locales/lt.ts +1 -0
  86. package/src/v4/locales/mk.ts +1 -0
  87. package/src/v4/locales/ms.ts +1 -0
  88. package/src/v4/locales/ne.ts +1 -0
  89. package/src/v4/locales/nl.ts +1 -0
  90. package/src/v4/locales/nn.ts +1 -0
  91. package/src/v4/locales/no.ts +1 -0
  92. package/src/v4/locales/ota.ts +1 -0
  93. package/src/v4/locales/pl.ts +1 -0
  94. package/src/v4/locales/ps.ts +1 -0
  95. package/src/v4/locales/pt-BR.ts +1 -0
  96. package/src/v4/locales/pt.ts +1 -0
  97. package/src/v4/locales/ro.ts +1 -0
  98. package/src/v4/locales/ru.ts +1 -0
  99. package/src/v4/locales/sk.ts +1 -0
  100. package/src/v4/locales/sl.ts +1 -0
  101. package/src/v4/locales/sv.ts +1 -0
  102. package/src/v4/locales/ta.ts +1 -0
  103. package/src/v4/locales/th.ts +1 -0
  104. package/src/v4/locales/tk.ts +1 -0
  105. package/src/v4/locales/tr.ts +1 -0
  106. package/src/v4/locales/uk.ts +1 -0
  107. package/src/v4/locales/ur.ts +1 -0
  108. package/src/v4/locales/uz.ts +1 -0
  109. package/src/v4/locales/vi.ts +1 -0
  110. package/src/v4/locales/yo.ts +1 -0
  111. package/src/v4/locales/zh-CN.ts +1 -0
  112. package/src/v4/locales/zh-TW.ts +1 -0
  113. package/src/v4/mini/checks.ts +0 -1
  114. package/src/v4/mini/external.ts +2 -0
  115. package/src/v4/mini/schemas.ts +38 -2
  116. package/src/v4/mini/tests/checks.test.ts +1 -1
  117. package/src/v4/mini/tests/computed.test.ts +11 -17
  118. package/src/v4/mini/tests/index.test.ts +8 -0
  119. package/src/v4/mini/tests/string.test.ts +16 -0
  120. package/v3/ZodError.cjs +19 -0
  121. package/v3/errors.cjs +19 -0
  122. package/v3/external.cjs +19 -0
  123. package/v3/helpers/enumUtil.cjs +19 -0
  124. package/v3/helpers/errorUtil.cjs +20 -1
  125. package/v3/helpers/errorUtil.js +1 -1
  126. package/v3/helpers/parseUtil.cjs +19 -0
  127. package/v3/helpers/partialUtil.cjs +19 -0
  128. package/v3/helpers/typeAliases.cjs +19 -0
  129. package/v3/helpers/util.cjs +19 -0
  130. package/v3/index.cjs +19 -0
  131. package/v3/standard-schema.cjs +19 -0
  132. package/v3/types.cjs +19 -0
  133. package/v4/classic/checks.cjs +20 -2
  134. package/v4/classic/checks.d.cts +1 -1
  135. package/v4/classic/checks.d.ts +1 -1
  136. package/v4/classic/checks.js +1 -1
  137. package/v4/classic/coerce.cjs +19 -0
  138. package/v4/classic/compat.cjs +19 -0
  139. package/v4/classic/deep-partial.cjs +19 -0
  140. package/v4/classic/errors.cjs +19 -0
  141. package/v4/classic/external.cjs +22 -1
  142. package/v4/classic/external.d.cts +1 -1
  143. package/v4/classic/external.d.ts +1 -1
  144. package/v4/classic/external.js +1 -1
  145. package/v4/classic/from-json-schema.cjs +264 -24
  146. package/v4/classic/from-json-schema.js +245 -24
  147. package/v4/classic/in-out.cjs +19 -0
  148. package/v4/classic/index.cjs +19 -0
  149. package/v4/classic/iso.cjs +19 -0
  150. package/v4/classic/parse.cjs +19 -0
  151. package/v4/classic/schemas.cjs +100 -31
  152. package/v4/classic/schemas.d.cts +17 -1
  153. package/v4/classic/schemas.d.ts +17 -1
  154. package/v4/classic/schemas.js +77 -29
  155. package/v4/core/api.cjs +37 -2
  156. package/v4/core/api.d.cts +6 -4
  157. package/v4/core/api.d.ts +6 -4
  158. package/v4/core/api.js +17 -2
  159. package/v4/core/checks.cjs +19 -96
  160. package/v4/core/checks.d.cts +1 -1
  161. package/v4/core/checks.d.ts +1 -1
  162. package/v4/core/checks.js +0 -96
  163. package/v4/core/compile.cjs +164 -65
  164. package/v4/core/compile.d.cts +18 -4
  165. package/v4/core/compile.d.ts +18 -4
  166. package/v4/core/compile.js +145 -66
  167. package/v4/core/core.cjs +19 -0
  168. package/v4/core/doc.cjs +26 -2
  169. package/v4/core/doc.js +7 -2
  170. package/v4/core/errors.cjs +21 -5
  171. package/v4/core/errors.js +2 -5
  172. package/v4/core/index.cjs +19 -0
  173. package/v4/core/json-schema-generator.cjs +20 -1
  174. package/v4/core/json-schema-generator.js +2 -2
  175. package/v4/core/json-schema-processors.cjs +215 -63
  176. package/v4/core/json-schema-processors.d.cts +17 -0
  177. package/v4/core/json-schema-processors.d.ts +17 -0
  178. package/v4/core/json-schema-processors.js +195 -64
  179. package/v4/core/json-schema.cjs +19 -0
  180. package/v4/core/memoizer.cjs +204 -36
  181. package/v4/core/memoizer.js +162 -36
  182. package/v4/core/parse.cjs +56 -16
  183. package/v4/core/parse.js +37 -16
  184. package/v4/core/regexes.cjs +29 -6
  185. package/v4/core/regexes.d.cts +2 -0
  186. package/v4/core/regexes.d.ts +2 -0
  187. package/v4/core/regexes.js +8 -4
  188. package/v4/core/registries.cjs +19 -0
  189. package/v4/core/schemas.cjs +285 -66
  190. package/v4/core/schemas.d.cts +34 -3
  191. package/v4/core/schemas.d.ts +34 -3
  192. package/v4/core/schemas.js +264 -65
  193. package/v4/core/standard-schema.cjs +19 -0
  194. package/v4/core/to-json-schema.cjs +29 -7
  195. package/v4/core/to-json-schema.d.cts +7 -3
  196. package/v4/core/to-json-schema.d.ts +7 -3
  197. package/v4/core/to-json-schema.js +8 -7
  198. package/v4/core/util.cjs +181 -143
  199. package/v4/core/util.d.cts +25 -4
  200. package/v4/core/util.d.ts +25 -4
  201. package/v4/core/util.js +160 -143
  202. package/v4/core/versions.cjs +21 -2
  203. package/v4/core/versions.d.cts +1 -1
  204. package/v4/core/versions.d.ts +1 -1
  205. package/v4/core/versions.js +2 -2
  206. package/v4/core/visit.cjs +31 -0
  207. package/v4/core/visit.js +12 -0
  208. package/v4/index.cjs +19 -0
  209. package/v4/locales/ar.cjs +1 -0
  210. package/v4/locales/ar.js +1 -0
  211. package/v4/locales/az.cjs +1 -0
  212. package/v4/locales/az.js +1 -0
  213. package/v4/locales/be.cjs +1 -0
  214. package/v4/locales/be.js +1 -0
  215. package/v4/locales/bg.cjs +1 -0
  216. package/v4/locales/bg.js +1 -0
  217. package/v4/locales/bn.cjs +1 -0
  218. package/v4/locales/bn.js +1 -0
  219. package/v4/locales/ca.cjs +1 -0
  220. package/v4/locales/ca.js +1 -0
  221. package/v4/locales/ckb.cjs +1 -0
  222. package/v4/locales/ckb.js +1 -0
  223. package/v4/locales/cs.cjs +1 -0
  224. package/v4/locales/cs.js +1 -0
  225. package/v4/locales/da.cjs +1 -0
  226. package/v4/locales/da.js +1 -0
  227. package/v4/locales/de.cjs +1 -0
  228. package/v4/locales/de.js +1 -0
  229. package/v4/locales/el.cjs +1 -0
  230. package/v4/locales/el.js +1 -0
  231. package/v4/locales/en.cjs +1 -0
  232. package/v4/locales/en.js +1 -0
  233. package/v4/locales/eo.cjs +1 -0
  234. package/v4/locales/eo.js +1 -0
  235. package/v4/locales/es.cjs +1 -0
  236. package/v4/locales/es.js +1 -0
  237. package/v4/locales/fa.cjs +1 -0
  238. package/v4/locales/fa.js +1 -0
  239. package/v4/locales/fi.cjs +1 -0
  240. package/v4/locales/fi.js +1 -0
  241. package/v4/locales/fr-CA.cjs +1 -0
  242. package/v4/locales/fr-CA.js +1 -0
  243. package/v4/locales/fr.cjs +1 -0
  244. package/v4/locales/fr.js +1 -0
  245. package/v4/locales/gu.cjs +1 -0
  246. package/v4/locales/gu.js +1 -0
  247. package/v4/locales/he.cjs +1 -0
  248. package/v4/locales/he.js +1 -0
  249. package/v4/locales/hi.cjs +1 -0
  250. package/v4/locales/hi.js +1 -0
  251. package/v4/locales/hr.cjs +1 -0
  252. package/v4/locales/hr.js +1 -0
  253. package/v4/locales/hu.cjs +1 -0
  254. package/v4/locales/hu.js +1 -0
  255. package/v4/locales/hy.cjs +1 -0
  256. package/v4/locales/hy.js +1 -0
  257. package/v4/locales/id.cjs +1 -0
  258. package/v4/locales/id.js +1 -0
  259. package/v4/locales/index.cjs +19 -0
  260. package/v4/locales/is.cjs +1 -0
  261. package/v4/locales/is.js +1 -0
  262. package/v4/locales/it.cjs +1 -0
  263. package/v4/locales/it.js +1 -0
  264. package/v4/locales/ja.cjs +1 -0
  265. package/v4/locales/ja.js +1 -0
  266. package/v4/locales/ka.cjs +1 -0
  267. package/v4/locales/ka.js +1 -0
  268. package/v4/locales/km.cjs +1 -0
  269. package/v4/locales/km.js +1 -0
  270. package/v4/locales/kn.cjs +1 -0
  271. package/v4/locales/kn.js +1 -0
  272. package/v4/locales/ko.cjs +1 -0
  273. package/v4/locales/ko.js +1 -0
  274. package/v4/locales/lt.cjs +1 -0
  275. package/v4/locales/lt.js +1 -0
  276. package/v4/locales/mk.cjs +1 -0
  277. package/v4/locales/mk.js +1 -0
  278. package/v4/locales/ms.cjs +1 -0
  279. package/v4/locales/ms.js +1 -0
  280. package/v4/locales/ne.cjs +1 -0
  281. package/v4/locales/ne.js +1 -0
  282. package/v4/locales/nl.cjs +1 -0
  283. package/v4/locales/nl.js +1 -0
  284. package/v4/locales/nn.cjs +1 -0
  285. package/v4/locales/nn.js +1 -0
  286. package/v4/locales/no.cjs +1 -0
  287. package/v4/locales/no.js +1 -0
  288. package/v4/locales/ota.cjs +1 -0
  289. package/v4/locales/ota.js +1 -0
  290. package/v4/locales/pl.cjs +1 -0
  291. package/v4/locales/pl.js +1 -0
  292. package/v4/locales/ps.cjs +1 -0
  293. package/v4/locales/ps.js +1 -0
  294. package/v4/locales/pt-BR.cjs +1 -0
  295. package/v4/locales/pt-BR.js +1 -0
  296. package/v4/locales/pt.cjs +1 -0
  297. package/v4/locales/pt.js +1 -0
  298. package/v4/locales/ro.cjs +1 -0
  299. package/v4/locales/ro.js +1 -0
  300. package/v4/locales/ru.cjs +1 -0
  301. package/v4/locales/ru.js +1 -0
  302. package/v4/locales/sk.cjs +1 -0
  303. package/v4/locales/sk.js +1 -0
  304. package/v4/locales/sl.cjs +1 -0
  305. package/v4/locales/sl.js +1 -0
  306. package/v4/locales/sv.cjs +1 -0
  307. package/v4/locales/sv.js +1 -0
  308. package/v4/locales/ta.cjs +1 -0
  309. package/v4/locales/ta.js +1 -0
  310. package/v4/locales/th.cjs +1 -0
  311. package/v4/locales/th.js +1 -0
  312. package/v4/locales/tk.cjs +1 -0
  313. package/v4/locales/tk.js +1 -0
  314. package/v4/locales/tr.cjs +1 -0
  315. package/v4/locales/tr.js +1 -0
  316. package/v4/locales/uk.cjs +1 -0
  317. package/v4/locales/uk.js +1 -0
  318. package/v4/locales/ur.cjs +1 -0
  319. package/v4/locales/ur.js +1 -0
  320. package/v4/locales/uz.cjs +1 -0
  321. package/v4/locales/uz.js +1 -0
  322. package/v4/locales/vi.cjs +1 -0
  323. package/v4/locales/vi.js +1 -0
  324. package/v4/locales/yo.cjs +1 -0
  325. package/v4/locales/yo.js +1 -0
  326. package/v4/locales/zh-CN.cjs +1 -0
  327. package/v4/locales/zh-CN.js +1 -0
  328. package/v4/locales/zh-TW.cjs +1 -0
  329. package/v4/locales/zh-TW.js +1 -0
  330. package/v4/mini/checks.cjs +20 -2
  331. package/v4/mini/checks.d.cts +1 -1
  332. package/v4/mini/checks.d.ts +1 -1
  333. package/v4/mini/checks.js +1 -1
  334. package/v4/mini/coerce.cjs +19 -0
  335. package/v4/mini/deep-partial.cjs +19 -0
  336. package/v4/mini/external.cjs +22 -1
  337. package/v4/mini/external.d.cts +1 -1
  338. package/v4/mini/external.d.ts +1 -1
  339. package/v4/mini/external.js +1 -1
  340. package/v4/mini/in-out.cjs +19 -0
  341. package/v4/mini/index.cjs +19 -0
  342. package/v4/mini/iso.cjs +19 -0
  343. package/v4/mini/parse.cjs +19 -0
  344. package/v4/mini/schemas.cjs +42 -4
  345. package/v4/mini/schemas.d.cts +8 -0
  346. package/v4/mini/schemas.d.ts +8 -0
  347. package/v4/mini/schemas.js +19 -2
  348. package/v4-mini/index.cjs +19 -0
@@ -2,6 +2,7 @@ import type * as checks from "./checks.js";
2
2
  import type * as JSONSchema from "./json-schema.js";
3
3
  import * as regexes from "./regexes.js";
4
4
  import type { $ZodRegistry } from "./registries.js";
5
+ import { base64Charset, base64urlCharset } from "./schemas.js";
5
6
  import type * as schemas from "./schemas.js";
6
7
  import {
7
8
  type ProcessParams,
@@ -15,9 +16,116 @@ import {
15
16
  finalize,
16
17
  handleUnrepresentable,
17
18
  initializeContext,
18
- process,
19
+ isTransforming,
20
+ processSchema,
19
21
  } from "./to-json-schema.js";
20
- import { assignProp, getEnumValues } from "./util.js";
22
+ import { BIGINT_FORMAT_RANGES, NUMBER_FORMAT_RANGES, assignProp, getEnumValues } from "./util.js";
23
+
24
+ // ==================== CHECK AGGREGATION ====================
25
+
26
+ // constraint metadata comes from folding `def.checks` here, not from trusting `_zod.bag`: the bag is written by each check's own `onattach` in chain order, which has repeatedly produced order-dependent and lossy values (#6550). runtime checks are a conjunction, so every merge below is the conjunction's — tightest bound wins, patterns union, divisors collect, mime lists intersect
27
+ type Bound = number | bigint | Date;
28
+
29
+ interface CheckAggregate<N extends Bound = Bound> {
30
+ minimum?: N;
31
+ maximum?: N;
32
+ exclusiveMinimum?: N;
33
+ exclusiveMaximum?: N;
34
+ multipleOf?: number[];
35
+ format?: string;
36
+ // ORed across formats: any integer format keeps the conjunction integer, whatever format lands last
37
+ isInt?: boolean;
38
+ patterns?: Set<RegExp>;
39
+ mime?: string[];
40
+ contentEncoding?: string;
41
+ // a datetime that drops the offset or the seconds accepts what `date-time` forbids, so the keyword is withheld
42
+ laxFormat?: boolean;
43
+ }
44
+
45
+ const narrowMin = (agg: CheckAggregate, key: "minimum" | "exclusiveMinimum", value: Bound): void => {
46
+ if (agg[key] === undefined || value > agg[key]) agg[key] = value;
47
+ };
48
+ const narrowMax = (agg: CheckAggregate, key: "maximum" | "exclusiveMaximum", value: Bound): void => {
49
+ if (agg[key] === undefined || value < agg[key]) agg[key] = value;
50
+ };
51
+ const narrowBoth = (agg: CheckAggregate, value: number): void => {
52
+ narrowMin(agg, "minimum", value);
53
+ narrowMax(agg, "maximum", value);
54
+ };
55
+ const addDivisor = (agg: CheckAggregate, value: number): void => {
56
+ agg.multipleOf ??= [];
57
+ if (!agg.multipleOf.includes(value)) agg.multipleOf.push(value);
58
+ };
59
+ const addPattern = (agg: CheckAggregate, pattern: RegExp): void => {
60
+ agg.patterns ??= new Set();
61
+ agg.patterns.add(pattern);
62
+ };
63
+ const intersectMime = (agg: CheckAggregate, mime: string[]): void => {
64
+ agg.mime = agg.mime ? agg.mime.filter((m) => mime.includes(m)) : [...mime];
65
+ };
66
+ // last-wins, matching the bag's historical write order; the flag keeps an integer format from being lost to a later float one
67
+ const setFormat = (agg: CheckAggregate, format: string): void => {
68
+ agg.format = format;
69
+ if (format.includes("int")) agg.isInt = true;
70
+ };
71
+
72
+ type Contributor = (agg: CheckAggregate, def: any) => void;
73
+ const minContributor: Contributor = (agg, def) => narrowMin(agg, "minimum", def.minimum);
74
+ const maxContributor: Contributor = (agg, def) => narrowMax(agg, "maximum", def.maximum);
75
+ const formatContributor =
76
+ (ranges: Record<string, [Bound, Bound]>): Contributor =>
77
+ (agg, def) => {
78
+ setFormat(agg, def.format);
79
+ const [minimum, maximum] = ranges[def.format]!;
80
+ narrowMin(agg, "minimum", minimum);
81
+ narrowMax(agg, "maximum", maximum);
82
+ };
83
+
84
+ const contributors: Record<string, Contributor> = {
85
+ greater_than: (agg, def) => narrowMin(agg, def.inclusive ? "minimum" : "exclusiveMinimum", def.value),
86
+ less_than: (agg, def) => narrowMax(agg, def.inclusive ? "maximum" : "exclusiveMaximum", def.value),
87
+ multiple_of: (agg, def) => addDivisor(agg, def.value),
88
+ number_format: formatContributor(NUMBER_FORMAT_RANGES),
89
+ bigint_format: formatContributor(BIGINT_FORMAT_RANGES),
90
+ min_length: minContributor,
91
+ max_length: maxContributor,
92
+ length_equals: (agg, def) => narrowBoth(agg, def.length),
93
+ min_size: minContributor,
94
+ max_size: maxContributor,
95
+ size_equals: (agg, def) => narrowBoth(agg, def.size),
96
+ string_format: (agg, def) => {
97
+ setFormat(agg, def.format);
98
+ if (def.pattern) addPattern(agg, def.pattern);
99
+ if (def.format === "base64" || def.format === "base64url") agg.contentEncoding = def.format;
100
+ if (def.local || def.precision === -1) agg.laxFormat = true;
101
+ },
102
+ mime_type: (agg, def) => intersectMime(agg, def.mime),
103
+ };
104
+
105
+ export function aggregateChecks<N extends Bound = Bound>(schema: schemas.$ZodType): CheckAggregate<N> {
106
+ const agg: CheckAggregate = {};
107
+ const def = schema._zod.def;
108
+ // a format schema is its own first check, same rule as $ZodType init
109
+ const list: checks.$ZodCheck[] = schema._zod.traits.has("$ZodCheck")
110
+ ? [schema as unknown as checks.$ZodCheck, ...(def.checks ?? [])]
111
+ : (def.checks ?? []);
112
+ for (const ch of list) contributors[ch._zod.def.check]?.(agg, ch._zod.def);
113
+
114
+ // reconcile with the bag so third-party onattach contributions still land; first-party residue is never tighter than the fold, so merging it back is idempotent for one and additive for the other
115
+ const bag = schema._zod.bag as Omit<CheckAggregate, "multipleOf"> & { multipleOf?: number };
116
+ if (bag.minimum !== undefined) narrowMin(agg, "minimum", bag.minimum);
117
+ if (bag.exclusiveMinimum !== undefined) narrowMin(agg, "exclusiveMinimum", bag.exclusiveMinimum);
118
+ if (bag.maximum !== undefined) narrowMax(agg, "maximum", bag.maximum);
119
+ if (bag.exclusiveMaximum !== undefined) narrowMax(agg, "exclusiveMaximum", bag.exclusiveMaximum);
120
+ if (bag.multipleOf !== undefined) addDivisor(agg, bag.multipleOf);
121
+ if (bag.format !== undefined) {
122
+ agg.format ??= bag.format;
123
+ if (bag.format.includes("int")) agg.isInt = true;
124
+ }
125
+ if (bag.mime) intersectMime(agg, bag.mime);
126
+ for (const pattern of bag.patterns ?? []) addPattern(agg, pattern);
127
+ return agg as CheckAggregate<N>;
128
+ }
21
129
 
22
130
  const formatMap: Partial<Record<checks.$ZodStringFormats, string | undefined>> = {
23
131
  guid: "uuid",
@@ -29,11 +137,17 @@ const formatMap: Partial<Record<checks.$ZodStringFormats, string | undefined>> =
29
137
 
30
138
  // ==================== SIMPLE TYPE PROCESSORS ====================
31
139
 
140
+ // the runtime patterns are lax so parse paths never overflow the regex stack; the emitted schema swaps in the exact block forms, which zod itself never executes
141
+ const exactPatterns: Map<RegExp, RegExp> = new Map([
142
+ [base64Charset, regexes.base64],
143
+ [base64urlCharset, regexes.base64url],
144
+ ]);
145
+ const exactPattern = (p: RegExp): RegExp => exactPatterns.get(p) ?? p;
146
+
32
147
  export const stringProcessor: Processor<schemas.$ZodString> = (schema, ctx, _json, _params) => {
33
148
  const json = _json as JSONSchema.StringSchema;
34
149
  json.type = "string";
35
- const { minimum, maximum, format, patterns, contentEncoding, laxFormat } = schema._zod
36
- .bag as schemas.$ZodStringInternals<unknown>["bag"];
150
+ const { minimum, maximum, format, patterns, contentEncoding, laxFormat } = aggregateChecks(schema);
37
151
  if (typeof minimum === "number") json.minLength = minimum;
38
152
  if (typeof maximum === "number") json.maxLength = maximum;
39
153
  // custom pattern overrides format
@@ -48,7 +162,7 @@ export const stringProcessor: Processor<schemas.$ZodString> = (schema, ctx, _jso
48
162
  }
49
163
  if (contentEncoding) json.contentEncoding = contentEncoding;
50
164
  if (patterns && patterns.size > 0) {
51
- const patternList = [...patterns];
165
+ const patternList = [...patterns].map(exactPattern);
52
166
  if (patternList.length === 1) json.pattern = patternList[0]!.source;
53
167
  else if (patternList.length > 1) {
54
168
  json.allOf = [
@@ -65,9 +179,8 @@ export const stringProcessor: Processor<schemas.$ZodString> = (schema, ctx, _jso
65
179
 
66
180
  export const numberProcessor: Processor<schemas.$ZodNumber> = (schema, ctx, _json, params) => {
67
181
  const json = _json as JSONSchema.NumberSchema | JSONSchema.IntegerSchema;
68
- const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;
69
- if (typeof format === "string" && format.includes("int")) json.type = "integer";
70
- else json.type = "number";
182
+ const { minimum, maximum, multipleOf, exclusiveMaximum, exclusiveMinimum, isInt } = aggregateChecks<number>(schema);
183
+ json.type = isInt ? "integer" : "number";
71
184
 
72
185
  // when both minimum and exclusiveMinimum exist, pick the more restrictive one
73
186
  const exMin = typeof exclusiveMinimum === "number" && exclusiveMinimum >= (minimum ?? Number.NEGATIVE_INFINITY);
@@ -96,17 +209,24 @@ export const numberProcessor: Processor<schemas.$ZodNumber> = (schema, ctx, _jso
96
209
  json.maximum = maximum;
97
210
  }
98
211
 
99
- if (typeof multipleOf === "number") {
212
+ if (multipleOf) {
100
213
  // JSON Schema requires a divisor strictly greater than zero, and a non-finite one does not survive JSON at all. A negative divisor accepts exactly what its absolute value accepts, so it still maps; zero, NaN and Infinity have no keyword form.
101
- if (Number.isFinite(multipleOf) && multipleOf !== 0) json.multipleOf = Math.abs(multipleOf);
102
- else
103
- handleUnrepresentable(
104
- schema,
105
- ctx,
106
- json,
107
- params,
108
- `A multipleOf divisor of ${multipleOf} cannot be represented in JSON Schema`
109
- );
214
+ const divisors = new Set<number>();
215
+ for (const divisor of multipleOf) {
216
+ if (Number.isFinite(divisor) && divisor !== 0) divisors.add(Math.abs(divisor));
217
+ else
218
+ handleUnrepresentable(
219
+ schema,
220
+ ctx,
221
+ json,
222
+ params,
223
+ `A multipleOf divisor of ${divisor} cannot be represented in JSON Schema`
224
+ );
225
+ }
226
+ // chained divisors are a conjunction the keyword cannot carry alone, so extras ride an allOf, same as stacked patterns
227
+ const [first, ...rest] = divisors;
228
+ if (first !== undefined) json.multipleOf = first;
229
+ if (rest.length) json.allOf = [...(json.allOf ?? []), ...rest.map((m) => ({ multipleOf: m }))];
110
230
  }
111
231
  };
112
232
 
@@ -229,26 +349,19 @@ export const templateLiteralProcessor: Processor<schemas.$ZodTemplateLiteral> =
229
349
 
230
350
  export const fileProcessor: Processor<schemas.$ZodFile> = (schema, _ctx, json, _params) => {
231
351
  const _json = json as JSONSchema.StringSchema;
232
- const file: JSONSchema.StringSchema = {
233
- type: "string",
234
- format: "binary",
235
- contentEncoding: "binary",
236
- };
352
+ _json.type = "string";
353
+ _json.format = "binary";
354
+ _json.contentEncoding = "binary";
237
355
 
238
- const { minimum, maximum, mime } = schema._zod.bag as schemas.$ZodFileInternals["bag"];
239
- if (minimum !== undefined) file.minLength = minimum;
240
- if (maximum !== undefined) file.maxLength = maximum;
241
- if (mime) {
242
- if (mime.length === 1) {
243
- file.contentMediaType = mime[0]!;
244
- Object.assign(_json, file);
245
- } else {
246
- Object.assign(_json, file); // shared props at root
247
- _json.anyOf = mime.map((m) => ({ contentMediaType: m })); // only contentMediaType differs
248
- }
249
- } else {
250
- Object.assign(_json, file);
251
- }
356
+ const { minimum, maximum, mime } = aggregateChecks<number>(schema);
357
+ if (minimum !== undefined) _json.minLength = minimum;
358
+ if (maximum !== undefined) _json.maxLength = maximum;
359
+ if (!mime) return;
360
+ // an empty intersection means the mime checks share no value, so nothing passes at runtime; `anyOf` must be non-empty, so the false schema is `not: {}`
361
+ if (mime.length === 0) _json.not = {};
362
+ else if (mime.length === 1) _json.contentMediaType = mime[0]!;
363
+ // only contentMediaType differs, so the shared props stay at the root
364
+ else _json.anyOf = mime.map((m) => ({ contentMediaType: m }));
252
365
  };
253
366
 
254
367
  export const successProcessor: Processor<schemas.$ZodSuccess> = (_schema, _ctx, json, _params) => {
@@ -280,12 +393,12 @@ export const setProcessor: Processor<schemas.$ZodSet> = (schema, ctx, json, para
280
393
  export const arrayProcessor: Processor<schemas.$ZodArray> = (schema, ctx, _json, params) => {
281
394
  const json = _json as JSONSchema.ArraySchema;
282
395
  const def = schema._zod.def as schemas.$ZodArrayDef;
283
- const { minimum, maximum } = schema._zod.bag;
396
+ const { minimum, maximum } = aggregateChecks(schema);
284
397
  if (typeof minimum === "number") json.minItems = minimum;
285
398
  if (typeof maximum === "number") json.maxItems = maximum;
286
399
 
287
400
  json.type = "array";
288
- json.items = process(def.element, ctx as any, {
401
+ json.items = processSchema(def.element, ctx as any, {
289
402
  ...params,
290
403
  path: [...params.path, "items"],
291
404
  });
@@ -329,7 +442,7 @@ export const objectProcessor: Processor<schemas.$ZodObject> = (schema, ctx, _jso
329
442
  assignProp(
330
443
  json.properties,
331
444
  key,
332
- process(shape[key]!, ctx as any, {
445
+ processSchema(shape[key]!, ctx as any, {
333
446
  ...params,
334
447
  path: [...params.path, "properties", key],
335
448
  })
@@ -361,19 +474,67 @@ export const objectProcessor: Processor<schemas.$ZodObject> = (schema, ctx, _jso
361
474
  // regular
362
475
  if (ctx.io === "output") json.additionalProperties = false;
363
476
  } else if (def.catchall) {
364
- json.additionalProperties = process(def.catchall, ctx as any, {
477
+ json.additionalProperties = processSchema(def.catchall, ctx as any, {
365
478
  ...params,
366
479
  path: [...params.path, "additionalProperties"],
367
480
  });
368
481
  }
369
482
  };
370
483
 
484
+ // asserts named properties in place and passes everything else through, so no additionalProperties constraint is emitted
485
+ export const propertiesProcessor: Processor<schemas.$ZodProperties> = (schema, ctx, _json, params) => {
486
+ const json = _json as JSONSchema.ObjectSchema;
487
+ const def = schema._zod.def;
488
+
489
+ // dropping a symbol key silently would emit a schema that asserts less than this one does
490
+ if (
491
+ Object.getOwnPropertySymbols(def.shape).length &&
492
+ handleUnrepresentable(schema, ctx, json, params, "Symbol keys cannot be represented in JSON Schema")
493
+ ) {
494
+ return;
495
+ }
496
+
497
+ // the parsed value is the input, so an output-mode emission would describe a transformed value this schema never returns
498
+ if (ctx.io === "output") {
499
+ for (const key in def.shape) {
500
+ if (
501
+ isTransforming(def.shape[key]!) &&
502
+ handleUnrepresentable(
503
+ schema,
504
+ ctx,
505
+ json,
506
+ params,
507
+ `z.properties() returns its input, so the output of a transforming schema at key "${key}" cannot be represented in JSON Schema`
508
+ )
509
+ ) {
510
+ return;
511
+ }
512
+ }
513
+ }
514
+
515
+ json.type = "object";
516
+ json.properties = {};
517
+ for (const key in def.shape) {
518
+ assignProp(
519
+ json.properties,
520
+ key,
521
+ processSchema(def.shape[key]!, ctx as any, {
522
+ ...params,
523
+ path: [...params.path, "properties", key],
524
+ })
525
+ );
526
+ }
527
+ // input-side optionality in both modes: the parsed value is the input, so a defaulted key that stays absent must not be required of the output either
528
+ const required = Object.keys(def.shape).filter((key) => inputOptin(def.shape[key]!) === undefined);
529
+ if (required.length > 0) json.required = required;
530
+ };
531
+
371
532
  export const unionProcessor: Processor<schemas.$ZodUnion> = (schema, ctx, json, params) => {
372
533
  const def = schema._zod.def as schemas.$ZodUnionDef;
373
534
  // Exclusive unions (inclusive === false) use oneOf (exactly one match) instead of anyOf (one or more matches). This includes both z.xor() and discriminated unions
374
535
  const isExclusive = def.inclusive === false;
375
536
  const options = def.options.map((x, i) =>
376
- process(x, ctx as any, {
537
+ processSchema(x, ctx as any, {
377
538
  ...params,
378
539
  path: [...params.path, isExclusive ? "oneOf" : "anyOf", i],
379
540
  })
@@ -387,11 +548,11 @@ export const unionProcessor: Processor<schemas.$ZodUnion> = (schema, ctx, json,
387
548
 
388
549
  export const intersectionProcessor: Processor<schemas.$ZodIntersection> = (schema, ctx, json, params) => {
389
550
  const def = schema._zod.def as schemas.$ZodIntersectionDef;
390
- const a = process(def.left, ctx as any, {
551
+ const a = processSchema(def.left, ctx as any, {
391
552
  ...params,
392
553
  path: [...params.path, "allOf", 0],
393
554
  });
394
- const b = process(def.right, ctx as any, {
555
+ const b = processSchema(def.right, ctx as any, {
395
556
  ...params,
396
557
  path: [...params.path, "allOf", 1],
397
558
  });
@@ -416,13 +577,13 @@ export const tupleProcessor: Processor<schemas.$ZodTuple> = (schema, ctx, _json,
416
577
  ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
417
578
 
418
579
  const prefixItems = def.items.map((x, i) =>
419
- process(x, ctx as any, {
580
+ processSchema(x, ctx as any, {
420
581
  ...params,
421
582
  path: [...params.path, prefixPath, i],
422
583
  })
423
584
  );
424
585
  const rest = def.rest
425
- ? process(def.rest, ctx as any, {
586
+ ? processSchema(def.rest, ctx as any, {
426
587
  ...params,
427
588
  path: [...params.path, restPath, ...(ctx.target === "openapi-3.0" ? [def.items.length] : [])],
428
589
  })
@@ -469,10 +630,7 @@ export const tupleProcessor: Processor<schemas.$ZodTuple> = (schema, ctx, _json,
469
630
  }
470
631
 
471
632
  // explicit user-defined length checks take precedence
472
- const { minimum, maximum } = schema._zod.bag as {
473
- minimum?: number;
474
- maximum?: number;
475
- };
633
+ const { minimum, maximum } = aggregateChecks(schema);
476
634
  if (typeof minimum === "number") json.minItems = minimum;
477
635
  if (typeof maximum === "number") json.maxItems = maximum;
478
636
  };
@@ -563,23 +721,22 @@ export const recordProcessor: Processor<schemas.$ZodRecord> = (schema, ctx, _jso
563
721
 
564
722
  // For looseRecord with regex patterns, use patternProperties. This correctly represents "only validate keys matching the pattern" semantics and composes well with allOf (intersections)
565
723
  const keyType = def.keyType as schemas.$ZodTypes;
566
- const keyBag = keyType._zod.bag as schemas.$ZodStringInternals<unknown>["bag"] | undefined;
567
- const patterns = keyBag?.patterns;
724
+ const patterns = aggregateChecks(keyType).patterns;
568
725
 
569
726
  if (def.mode === "loose" && patterns && patterns.size > 0) {
570
727
  // Use patternProperties for looseRecord with regex patterns
571
- const valueSchema = process(def.valueType, ctx as any, {
728
+ const valueSchema = processSchema(def.valueType, ctx as any, {
572
729
  ...params,
573
730
  path: [...params.path, "patternProperties", "*"],
574
731
  });
575
732
  json.patternProperties = {};
576
733
  for (const pattern of patterns) {
577
- assignProp(json.patternProperties, pattern.source, valueSchema);
734
+ assignProp(json.patternProperties, exactPattern(pattern).source, valueSchema);
578
735
  }
579
736
  } else {
580
737
  // Default behavior: use propertyNames + additionalProperties
581
738
  if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
582
- json.propertyNames = process(def.keyType, ctx as any, {
739
+ json.propertyNames = processSchema(def.keyType, ctx as any, {
583
740
  ...params,
584
741
  path: [...params.path, "propertyNames"],
585
742
  });
@@ -591,7 +748,7 @@ export const recordProcessor: Processor<schemas.$ZodRecord> = (schema, ctx, _jso
591
748
  }
592
749
  pending.push(schema);
593
750
  }
594
- json.additionalProperties = process(def.valueType, ctx as any, {
751
+ json.additionalProperties = processSchema(def.valueType, ctx as any, {
595
752
  ...params,
596
753
  path: [...params.path, "additionalProperties"],
597
754
  });
@@ -614,7 +771,7 @@ export const recordProcessor: Processor<schemas.$ZodRecord> = (schema, ctx, _jso
614
771
 
615
772
  export const nullableProcessor: Processor<schemas.$ZodNullable> = (schema, ctx, json, params) => {
616
773
  const def = schema._zod.def as schemas.$ZodNullableDef;
617
- const inner = process(def.innerType, ctx as any, params);
774
+ const inner = processSchema(def.innerType, ctx as any, params);
618
775
  const seen = ctx.seen.get(schema)!;
619
776
  if (ctx.target === "openapi-3.0") {
620
777
  seen.ref = def.innerType;
@@ -626,7 +783,7 @@ export const nullableProcessor: Processor<schemas.$ZodNullable> = (schema, ctx,
626
783
 
627
784
  export const nonoptionalProcessor: Processor<schemas.$ZodNonOptional> = (schema, ctx, _json, params) => {
628
785
  const def = schema._zod.def as schemas.$ZodNonOptionalDef;
629
- process(def.innerType, ctx as any, params);
786
+ processSchema(def.innerType, ctx as any, params);
630
787
  const seen = ctx.seen.get(schema)!;
631
788
  seen.ref = def.innerType;
632
789
  };
@@ -655,7 +812,7 @@ function serializeDefaultValue(
655
812
 
656
813
  export const defaultProcessor: Processor<schemas.$ZodDefault> = (schema, ctx, json, params) => {
657
814
  const def = schema._zod.def as schemas.$ZodDefaultDef;
658
- process(def.innerType, ctx as any, params);
815
+ processSchema(def.innerType, ctx as any, params);
659
816
  const seen = ctx.seen.get(schema)!;
660
817
  seen.ref = def.innerType;
661
818
  const value = serializeDefaultValue(def.defaultValue, schema, ctx as ToJSONSchemaContext, json, params);
@@ -664,7 +821,7 @@ export const defaultProcessor: Processor<schemas.$ZodDefault> = (schema, ctx, js
664
821
 
665
822
  export const prefaultProcessor: Processor<schemas.$ZodPrefault> = (schema, ctx, json, params) => {
666
823
  const def = schema._zod.def as schemas.$ZodPrefaultDef;
667
- process(def.innerType, ctx as any, params);
824
+ processSchema(def.innerType, ctx as any, params);
668
825
  const seen = ctx.seen.get(schema)!;
669
826
  seen.ref = def.innerType;
670
827
  if (ctx.io !== "input") return;
@@ -674,7 +831,7 @@ export const prefaultProcessor: Processor<schemas.$ZodPrefault> = (schema, ctx,
674
831
 
675
832
  export const catchProcessor: Processor<schemas.$ZodCatch> = (schema, ctx, json, params) => {
676
833
  const def = schema._zod.def as schemas.$ZodCatchDef;
677
- process(def.innerType, ctx as any, params);
834
+ processSchema(def.innerType, ctx as any, params);
678
835
  const seen = ctx.seen.get(schema)!;
679
836
  seen.ref = def.innerType;
680
837
  let catchValue: any;
@@ -691,14 +848,14 @@ export const pipeProcessor: Processor<schemas.$ZodPipe> = (schema, ctx, _json, p
691
848
  const def = schema._zod.def as schemas.$ZodPipeDef;
692
849
  const inIsTransform = def.in._zod.traits.has("$ZodTransform");
693
850
  const innerType = ctx.io === "input" ? (inIsTransform ? def.out : def.in) : def.out;
694
- process(innerType, ctx as any, params);
851
+ processSchema(innerType, ctx as any, params);
695
852
  const seen = ctx.seen.get(schema)!;
696
853
  seen.ref = innerType;
697
854
  };
698
855
 
699
856
  export const readonlyProcessor: Processor<schemas.$ZodReadonly> = (schema, ctx, json, params) => {
700
857
  const def = schema._zod.def as schemas.$ZodReadonlyDef;
701
- process(def.innerType, ctx as any, params);
858
+ processSchema(def.innerType, ctx as any, params);
702
859
  const seen = ctx.seen.get(schema)!;
703
860
  seen.ref = def.innerType;
704
861
  json.readOnly = true;
@@ -706,21 +863,21 @@ export const readonlyProcessor: Processor<schemas.$ZodReadonly> = (schema, ctx,
706
863
 
707
864
  export const promiseProcessor: Processor<schemas.$ZodPromise> = (schema, ctx, _json, params) => {
708
865
  const def = schema._zod.def as schemas.$ZodPromiseDef;
709
- process(def.innerType, ctx as any, params);
866
+ processSchema(def.innerType, ctx as any, params);
710
867
  const seen = ctx.seen.get(schema)!;
711
868
  seen.ref = def.innerType;
712
869
  };
713
870
 
714
871
  export const optionalProcessor: Processor<schemas.$ZodOptional> = (schema, ctx, _json, params) => {
715
872
  const def = schema._zod.def as schemas.$ZodOptionalDef;
716
- process(def.innerType, ctx as any, params);
873
+ processSchema(def.innerType, ctx as any, params);
717
874
  const seen = ctx.seen.get(schema)!;
718
875
  seen.ref = def.innerType;
719
876
  };
720
877
 
721
878
  export const lazyProcessor: Processor<schemas.$ZodLazy> = (schema, ctx, _json, params) => {
722
879
  const innerType = (schema as schemas.$ZodLazy)._zod.innerType;
723
- process(innerType, ctx as any, params);
880
+ processSchema(innerType, ctx as any, params);
724
881
  const seen = ctx.seen.get(schema)!;
725
882
  seen.ref = innerType;
726
883
  };
@@ -747,6 +904,7 @@ export const allProcessors: Record<string, Processor<any>> = {
747
904
  file: fileProcessor,
748
905
  success: successProcessor,
749
906
  custom: customProcessor,
907
+ properties: propertiesProcessor,
750
908
  function: functionProcessor,
751
909
  transform: transformProcessor,
752
910
  map: mapProcessor,
@@ -792,7 +950,7 @@ export function toJSONSchema(
792
950
  // First pass: process all schemas to build the seen map
793
951
  for (const entry of registry._idmap.entries()) {
794
952
  const [_, schema] = entry;
795
- process(schema, ctx as any);
953
+ processSchema(schema, ctx as any);
796
954
  }
797
955
 
798
956
  const schemas: Record<string, JSONSchema.BaseSchema> = {};
@@ -824,7 +982,7 @@ export function toJSONSchema(
824
982
 
825
983
  // Single schema case
826
984
  const ctx = initializeContext({ ...params, processors: allProcessors });
827
- process(input, ctx as any);
985
+ processSchema(input, ctx as any);
828
986
  extractDefs(ctx as any, input);
829
987
  return finalize(ctx as any, input);
830
988
  }