zod 4.5.4 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) 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 +274 -0
  15. package/src/v4/classic/tests/enum.test.ts +9 -0
  16. package/src/v4/classic/tests/error.test.ts +130 -1
  17. package/src/v4/classic/tests/firstparty.test.ts +4 -0
  18. package/src/v4/classic/tests/from-json-schema.test.ts +157 -0
  19. package/src/v4/classic/tests/iban.test.ts +81 -0
  20. package/src/v4/classic/tests/instance-footprint.test.ts +18 -2
  21. package/src/v4/classic/tests/instanceof.test.ts +27 -7
  22. package/src/v4/classic/tests/issue-schema.test.ts +88 -56
  23. package/src/v4/classic/tests/object.test.ts +56 -0
  24. package/src/v4/classic/tests/optin-ladder.test.ts +3 -2
  25. package/src/v4/classic/tests/properties.test.ts +225 -0
  26. package/src/v4/classic/tests/recursive-types.test.ts +1 -1
  27. package/src/v4/classic/tests/standard-schema.test.ts +19 -0
  28. package/src/v4/classic/tests/string.test.ts +25 -0
  29. package/src/v4/classic/tests/template-literal.test.ts +33 -1
  30. package/src/v4/classic/tests/to-json-schema.test.ts +147 -3
  31. package/src/v4/classic/tests/validate.test.ts +569 -0
  32. package/src/v4/core/api.ts +30 -6
  33. package/src/v4/core/checks.ts +1 -101
  34. package/src/v4/core/compile.ts +193 -81
  35. package/src/v4/core/doc.ts +6 -2
  36. package/src/v4/core/errors.ts +3 -6
  37. package/src/v4/core/json-schema-generator.ts +2 -2
  38. package/src/v4/core/json-schema-processors.ts +226 -68
  39. package/src/v4/core/memoizer.ts +78 -28
  40. package/src/v4/core/parse.ts +37 -16
  41. package/src/v4/core/regexes.ts +10 -4
  42. package/src/v4/core/schemas.ts +314 -83
  43. package/src/v4/core/tests/cached.test.ts +54 -0
  44. package/src/v4/core/tests/compile-generative.test.ts +445 -0
  45. package/src/v4/core/tests/compile.test.ts +222 -2
  46. package/src/v4/core/tests/email-regex.test.ts +140 -0
  47. package/src/v4/core/tests/polyfill-collision.test.ts +46 -0
  48. package/src/v4/core/to-json-schema.ts +10 -8
  49. package/src/v4/core/util.ts +222 -147
  50. package/src/v4/core/versions.ts +2 -2
  51. package/src/v4/core/visit.ts +11 -0
  52. package/src/v4/locales/ar.ts +1 -0
  53. package/src/v4/locales/az.ts +1 -0
  54. package/src/v4/locales/be.ts +1 -0
  55. package/src/v4/locales/bg.ts +1 -0
  56. package/src/v4/locales/bn.ts +1 -0
  57. package/src/v4/locales/ca.ts +1 -0
  58. package/src/v4/locales/ckb.ts +1 -0
  59. package/src/v4/locales/cs.ts +1 -0
  60. package/src/v4/locales/da.ts +1 -0
  61. package/src/v4/locales/de.ts +1 -0
  62. package/src/v4/locales/el.ts +1 -0
  63. package/src/v4/locales/en.ts +1 -0
  64. package/src/v4/locales/eo.ts +1 -0
  65. package/src/v4/locales/es.ts +1 -0
  66. package/src/v4/locales/fa.ts +1 -0
  67. package/src/v4/locales/fi.ts +1 -0
  68. package/src/v4/locales/fr-CA.ts +1 -0
  69. package/src/v4/locales/fr.ts +1 -0
  70. package/src/v4/locales/gu.ts +1 -0
  71. package/src/v4/locales/he.ts +1 -0
  72. package/src/v4/locales/hi.ts +1 -0
  73. package/src/v4/locales/hr.ts +1 -0
  74. package/src/v4/locales/hu.ts +1 -0
  75. package/src/v4/locales/hy.ts +1 -0
  76. package/src/v4/locales/id.ts +1 -0
  77. package/src/v4/locales/is.ts +1 -0
  78. package/src/v4/locales/it.ts +1 -0
  79. package/src/v4/locales/ja.ts +1 -0
  80. package/src/v4/locales/ka.ts +1 -0
  81. package/src/v4/locales/km.ts +1 -0
  82. package/src/v4/locales/kn.ts +1 -0
  83. package/src/v4/locales/ko.ts +1 -0
  84. package/src/v4/locales/lt.ts +1 -0
  85. package/src/v4/locales/mk.ts +1 -0
  86. package/src/v4/locales/ms.ts +1 -0
  87. package/src/v4/locales/ne.ts +1 -0
  88. package/src/v4/locales/nl.ts +1 -0
  89. package/src/v4/locales/nn.ts +1 -0
  90. package/src/v4/locales/no.ts +1 -0
  91. package/src/v4/locales/ota.ts +1 -0
  92. package/src/v4/locales/pl.ts +1 -0
  93. package/src/v4/locales/ps.ts +1 -0
  94. package/src/v4/locales/pt-BR.ts +1 -0
  95. package/src/v4/locales/pt.ts +1 -0
  96. package/src/v4/locales/ro.ts +1 -0
  97. package/src/v4/locales/ru.ts +1 -0
  98. package/src/v4/locales/sk.ts +1 -0
  99. package/src/v4/locales/sl.ts +1 -0
  100. package/src/v4/locales/sv.ts +1 -0
  101. package/src/v4/locales/ta.ts +1 -0
  102. package/src/v4/locales/th.ts +1 -0
  103. package/src/v4/locales/tk.ts +1 -0
  104. package/src/v4/locales/tr.ts +1 -0
  105. package/src/v4/locales/uk.ts +1 -0
  106. package/src/v4/locales/ur.ts +1 -0
  107. package/src/v4/locales/uz.ts +1 -0
  108. package/src/v4/locales/vi.ts +1 -0
  109. package/src/v4/locales/yo.ts +1 -0
  110. package/src/v4/locales/zh-CN.ts +1 -0
  111. package/src/v4/locales/zh-TW.ts +1 -0
  112. package/src/v4/mini/checks.ts +0 -1
  113. package/src/v4/mini/external.ts +2 -0
  114. package/src/v4/mini/schemas.ts +38 -2
  115. package/src/v4/mini/tests/checks.test.ts +1 -1
  116. package/src/v4/mini/tests/computed.test.ts +11 -17
  117. package/src/v4/mini/tests/index.test.ts +8 -0
  118. package/src/v4/mini/tests/string.test.ts +16 -0
  119. package/v3/ZodError.cjs +19 -0
  120. package/v3/errors.cjs +19 -0
  121. package/v3/external.cjs +19 -0
  122. package/v3/helpers/enumUtil.cjs +19 -0
  123. package/v3/helpers/errorUtil.cjs +20 -1
  124. package/v3/helpers/errorUtil.js +1 -1
  125. package/v3/helpers/parseUtil.cjs +19 -0
  126. package/v3/helpers/partialUtil.cjs +19 -0
  127. package/v3/helpers/typeAliases.cjs +19 -0
  128. package/v3/helpers/util.cjs +19 -0
  129. package/v3/index.cjs +19 -0
  130. package/v3/standard-schema.cjs +19 -0
  131. package/v3/types.cjs +19 -0
  132. package/v4/classic/checks.cjs +20 -2
  133. package/v4/classic/checks.d.cts +1 -1
  134. package/v4/classic/checks.d.ts +1 -1
  135. package/v4/classic/checks.js +1 -1
  136. package/v4/classic/coerce.cjs +19 -0
  137. package/v4/classic/compat.cjs +19 -0
  138. package/v4/classic/deep-partial.cjs +19 -0
  139. package/v4/classic/errors.cjs +19 -0
  140. package/v4/classic/external.cjs +22 -1
  141. package/v4/classic/external.d.cts +1 -1
  142. package/v4/classic/external.d.ts +1 -1
  143. package/v4/classic/external.js +1 -1
  144. package/v4/classic/from-json-schema.cjs +264 -24
  145. package/v4/classic/from-json-schema.js +245 -24
  146. package/v4/classic/in-out.cjs +19 -0
  147. package/v4/classic/index.cjs +19 -0
  148. package/v4/classic/iso.cjs +19 -0
  149. package/v4/classic/parse.cjs +19 -0
  150. package/v4/classic/schemas.cjs +100 -31
  151. package/v4/classic/schemas.d.cts +17 -1
  152. package/v4/classic/schemas.d.ts +17 -1
  153. package/v4/classic/schemas.js +77 -29
  154. package/v4/core/api.cjs +37 -2
  155. package/v4/core/api.d.cts +6 -4
  156. package/v4/core/api.d.ts +6 -4
  157. package/v4/core/api.js +17 -2
  158. package/v4/core/checks.cjs +19 -96
  159. package/v4/core/checks.d.cts +1 -1
  160. package/v4/core/checks.d.ts +1 -1
  161. package/v4/core/checks.js +0 -96
  162. package/v4/core/compile.cjs +164 -65
  163. package/v4/core/compile.d.cts +18 -4
  164. package/v4/core/compile.d.ts +18 -4
  165. package/v4/core/compile.js +145 -66
  166. package/v4/core/core.cjs +19 -0
  167. package/v4/core/doc.cjs +26 -2
  168. package/v4/core/doc.js +7 -2
  169. package/v4/core/errors.cjs +21 -5
  170. package/v4/core/errors.js +2 -5
  171. package/v4/core/index.cjs +19 -0
  172. package/v4/core/json-schema-generator.cjs +20 -1
  173. package/v4/core/json-schema-generator.js +2 -2
  174. package/v4/core/json-schema-processors.cjs +215 -63
  175. package/v4/core/json-schema-processors.d.cts +17 -0
  176. package/v4/core/json-schema-processors.d.ts +17 -0
  177. package/v4/core/json-schema-processors.js +195 -64
  178. package/v4/core/json-schema.cjs +19 -0
  179. package/v4/core/memoizer.cjs +114 -23
  180. package/v4/core/memoizer.js +72 -23
  181. package/v4/core/parse.cjs +56 -16
  182. package/v4/core/parse.js +37 -16
  183. package/v4/core/regexes.cjs +29 -6
  184. package/v4/core/regexes.d.cts +2 -0
  185. package/v4/core/regexes.d.ts +2 -0
  186. package/v4/core/regexes.js +8 -4
  187. package/v4/core/registries.cjs +19 -0
  188. package/v4/core/schemas.cjs +285 -66
  189. package/v4/core/schemas.d.cts +34 -3
  190. package/v4/core/schemas.d.ts +34 -3
  191. package/v4/core/schemas.js +264 -65
  192. package/v4/core/standard-schema.cjs +19 -0
  193. package/v4/core/to-json-schema.cjs +29 -7
  194. package/v4/core/to-json-schema.d.cts +7 -3
  195. package/v4/core/to-json-schema.d.ts +7 -3
  196. package/v4/core/to-json-schema.js +8 -7
  197. package/v4/core/util.cjs +181 -143
  198. package/v4/core/util.d.cts +25 -4
  199. package/v4/core/util.d.ts +25 -4
  200. package/v4/core/util.js +160 -143
  201. package/v4/core/versions.cjs +21 -2
  202. package/v4/core/versions.d.cts +1 -1
  203. package/v4/core/versions.d.ts +1 -1
  204. package/v4/core/versions.js +2 -2
  205. package/v4/core/visit.cjs +31 -0
  206. package/v4/core/visit.js +12 -0
  207. package/v4/index.cjs +19 -0
  208. package/v4/locales/ar.cjs +1 -0
  209. package/v4/locales/ar.js +1 -0
  210. package/v4/locales/az.cjs +1 -0
  211. package/v4/locales/az.js +1 -0
  212. package/v4/locales/be.cjs +1 -0
  213. package/v4/locales/be.js +1 -0
  214. package/v4/locales/bg.cjs +1 -0
  215. package/v4/locales/bg.js +1 -0
  216. package/v4/locales/bn.cjs +1 -0
  217. package/v4/locales/bn.js +1 -0
  218. package/v4/locales/ca.cjs +1 -0
  219. package/v4/locales/ca.js +1 -0
  220. package/v4/locales/ckb.cjs +1 -0
  221. package/v4/locales/ckb.js +1 -0
  222. package/v4/locales/cs.cjs +1 -0
  223. package/v4/locales/cs.js +1 -0
  224. package/v4/locales/da.cjs +1 -0
  225. package/v4/locales/da.js +1 -0
  226. package/v4/locales/de.cjs +1 -0
  227. package/v4/locales/de.js +1 -0
  228. package/v4/locales/el.cjs +1 -0
  229. package/v4/locales/el.js +1 -0
  230. package/v4/locales/en.cjs +1 -0
  231. package/v4/locales/en.js +1 -0
  232. package/v4/locales/eo.cjs +1 -0
  233. package/v4/locales/eo.js +1 -0
  234. package/v4/locales/es.cjs +1 -0
  235. package/v4/locales/es.js +1 -0
  236. package/v4/locales/fa.cjs +1 -0
  237. package/v4/locales/fa.js +1 -0
  238. package/v4/locales/fi.cjs +1 -0
  239. package/v4/locales/fi.js +1 -0
  240. package/v4/locales/fr-CA.cjs +1 -0
  241. package/v4/locales/fr-CA.js +1 -0
  242. package/v4/locales/fr.cjs +1 -0
  243. package/v4/locales/fr.js +1 -0
  244. package/v4/locales/gu.cjs +1 -0
  245. package/v4/locales/gu.js +1 -0
  246. package/v4/locales/he.cjs +1 -0
  247. package/v4/locales/he.js +1 -0
  248. package/v4/locales/hi.cjs +1 -0
  249. package/v4/locales/hi.js +1 -0
  250. package/v4/locales/hr.cjs +1 -0
  251. package/v4/locales/hr.js +1 -0
  252. package/v4/locales/hu.cjs +1 -0
  253. package/v4/locales/hu.js +1 -0
  254. package/v4/locales/hy.cjs +1 -0
  255. package/v4/locales/hy.js +1 -0
  256. package/v4/locales/id.cjs +1 -0
  257. package/v4/locales/id.js +1 -0
  258. package/v4/locales/index.cjs +19 -0
  259. package/v4/locales/is.cjs +1 -0
  260. package/v4/locales/is.js +1 -0
  261. package/v4/locales/it.cjs +1 -0
  262. package/v4/locales/it.js +1 -0
  263. package/v4/locales/ja.cjs +1 -0
  264. package/v4/locales/ja.js +1 -0
  265. package/v4/locales/ka.cjs +1 -0
  266. package/v4/locales/ka.js +1 -0
  267. package/v4/locales/km.cjs +1 -0
  268. package/v4/locales/km.js +1 -0
  269. package/v4/locales/kn.cjs +1 -0
  270. package/v4/locales/kn.js +1 -0
  271. package/v4/locales/ko.cjs +1 -0
  272. package/v4/locales/ko.js +1 -0
  273. package/v4/locales/lt.cjs +1 -0
  274. package/v4/locales/lt.js +1 -0
  275. package/v4/locales/mk.cjs +1 -0
  276. package/v4/locales/mk.js +1 -0
  277. package/v4/locales/ms.cjs +1 -0
  278. package/v4/locales/ms.js +1 -0
  279. package/v4/locales/ne.cjs +1 -0
  280. package/v4/locales/ne.js +1 -0
  281. package/v4/locales/nl.cjs +1 -0
  282. package/v4/locales/nl.js +1 -0
  283. package/v4/locales/nn.cjs +1 -0
  284. package/v4/locales/nn.js +1 -0
  285. package/v4/locales/no.cjs +1 -0
  286. package/v4/locales/no.js +1 -0
  287. package/v4/locales/ota.cjs +1 -0
  288. package/v4/locales/ota.js +1 -0
  289. package/v4/locales/pl.cjs +1 -0
  290. package/v4/locales/pl.js +1 -0
  291. package/v4/locales/ps.cjs +1 -0
  292. package/v4/locales/ps.js +1 -0
  293. package/v4/locales/pt-BR.cjs +1 -0
  294. package/v4/locales/pt-BR.js +1 -0
  295. package/v4/locales/pt.cjs +1 -0
  296. package/v4/locales/pt.js +1 -0
  297. package/v4/locales/ro.cjs +1 -0
  298. package/v4/locales/ro.js +1 -0
  299. package/v4/locales/ru.cjs +1 -0
  300. package/v4/locales/ru.js +1 -0
  301. package/v4/locales/sk.cjs +1 -0
  302. package/v4/locales/sk.js +1 -0
  303. package/v4/locales/sl.cjs +1 -0
  304. package/v4/locales/sl.js +1 -0
  305. package/v4/locales/sv.cjs +1 -0
  306. package/v4/locales/sv.js +1 -0
  307. package/v4/locales/ta.cjs +1 -0
  308. package/v4/locales/ta.js +1 -0
  309. package/v4/locales/th.cjs +1 -0
  310. package/v4/locales/th.js +1 -0
  311. package/v4/locales/tk.cjs +1 -0
  312. package/v4/locales/tk.js +1 -0
  313. package/v4/locales/tr.cjs +1 -0
  314. package/v4/locales/tr.js +1 -0
  315. package/v4/locales/uk.cjs +1 -0
  316. package/v4/locales/uk.js +1 -0
  317. package/v4/locales/ur.cjs +1 -0
  318. package/v4/locales/ur.js +1 -0
  319. package/v4/locales/uz.cjs +1 -0
  320. package/v4/locales/uz.js +1 -0
  321. package/v4/locales/vi.cjs +1 -0
  322. package/v4/locales/vi.js +1 -0
  323. package/v4/locales/yo.cjs +1 -0
  324. package/v4/locales/yo.js +1 -0
  325. package/v4/locales/zh-CN.cjs +1 -0
  326. package/v4/locales/zh-CN.js +1 -0
  327. package/v4/locales/zh-TW.cjs +1 -0
  328. package/v4/locales/zh-TW.js +1 -0
  329. package/v4/mini/checks.cjs +20 -2
  330. package/v4/mini/checks.d.cts +1 -1
  331. package/v4/mini/checks.d.ts +1 -1
  332. package/v4/mini/checks.js +1 -1
  333. package/v4/mini/coerce.cjs +19 -0
  334. package/v4/mini/deep-partial.cjs +19 -0
  335. package/v4/mini/external.cjs +22 -1
  336. package/v4/mini/external.d.cts +1 -1
  337. package/v4/mini/external.d.ts +1 -1
  338. package/v4/mini/external.js +1 -1
  339. package/v4/mini/in-out.cjs +19 -0
  340. package/v4/mini/index.cjs +19 -0
  341. package/v4/mini/iso.cjs +19 -0
  342. package/v4/mini/parse.cjs +19 -0
  343. package/v4/mini/schemas.cjs +42 -4
  344. package/v4/mini/schemas.d.cts +8 -0
  345. package/v4/mini/schemas.d.ts +8 -0
  346. package/v4/mini/schemas.js +19 -2
  347. package/v4-mini/index.cjs +19 -0
@@ -1,7 +1,15 @@
1
- import { expect, test } from "vitest";
1
+ import { expect, expectTypeOf, test } from "vitest";
2
2
 
3
3
  import * as z from "../../index.js";
4
- import { ZodCompileAsyncError, ZodCompileUnsupportedError, compile } from "../compile.js";
4
+ import {
5
+ INVALID,
6
+ ZodCompileAsyncError,
7
+ ZodCompileUnsupportedError,
8
+ compile,
9
+ compileFn,
10
+ withParser,
11
+ } from "../compile.js";
12
+ import { $ZodAsyncError } from "../core.js";
5
13
 
6
14
  // Differential helper: assert compiled schema matches the original on a value.
7
15
  function expectMatch(schema: z.ZodType, value: unknown) {
@@ -1350,6 +1358,29 @@ test("runtime island inside array element", () => {
1350
1358
  invalid(aot, ["a", true]);
1351
1359
  });
1352
1360
 
1361
+ test("an island thrown from inside an indented block leaves the indent level alone", () => {
1362
+ // nullable opens an indented block, coerce throws inside it, and the property islands the pair — two of them so a leak compounds
1363
+ const schema = z.object({ a: z.coerce.number().nullable(), b: z.coerce.number().nullable(), tail: z.string() });
1364
+ expect(compileFn(schema, { debug: true }).code).toMatchInlineSnapshot(`
1365
+ "// Constants: INVALID, c0, c1, c2
1366
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return INVALID;
1367
+ const v0 = input["a"];
1368
+ if (!("a" in input)) return INVALID;
1369
+ const v1 = c1(c0, v0);
1370
+ if (v1 === INVALID) return INVALID;
1371
+ const v2 = input["b"];
1372
+ if (!("b" in input)) return INVALID;
1373
+ const v3 = c1(c2, v2);
1374
+ if (v3 === INVALID) return INVALID;
1375
+ const v4 = input["tail"];
1376
+ if (typeof v4 !== "string") return INVALID;
1377
+ const v5 = { "a": v1, "b": v3, "tail": v4 };
1378
+ return v5;"
1379
+ `);
1380
+ expectMatch(schema, { a: "1", b: null, tail: "x" });
1381
+ expectMatch(schema, { a: "nope", b: null, tail: "x" });
1382
+ });
1383
+
1353
1384
  test("url string-format check never assigns to its accessor", () => {
1354
1385
  // const accessor (array element): previously emitted `elem = normalized`,
1355
1386
  // a TypeError on valid input.
@@ -1417,6 +1448,26 @@ test("xor and custom when-gated checks force runtime fallback", () => {
1417
1448
  expectMatch(z.object({ n: gated }), { n: 3 }); // runtime skips the gated check
1418
1449
  });
1419
1450
 
1451
+ test("compiled parse surfaces the interpreter's throws through the fallback", () => {
1452
+ // an unmergeable intersection bails to INVALID, and the fallback it hands to raises the interpreter's own error
1453
+ const unmergeable = compile(
1454
+ z.intersection(
1455
+ z.number(),
1456
+ z.number().transform((x) => x + 1)
1457
+ ),
1458
+ { strict: true }
1459
+ );
1460
+ expect(() => unmergeable.parse(1234)).toThrowErrorMatchingInlineSnapshot(
1461
+ `[Error: Unmergable intersection. Error path: []]`
1462
+ );
1463
+
1464
+ // a when-gated check islands at codegen, and the island's INVALID for an async run reaches the same fallback
1465
+ const gated = z.number().refine(() => Promise.resolve(true));
1466
+ (gated._zod.def.checks![0]!._zod.def as { when?: unknown }).when = () => true;
1467
+ const compiled = compile(z.object({ n: gated }), { strict: true });
1468
+ expect(() => compiled.parse({ n: 3 })).toThrow($ZodAsyncError);
1469
+ });
1470
+
1420
1471
  test("strict objects reject inherited enumerable keys like the runtime", () => {
1421
1472
  const schema = z.strictObject({ a: z.string() });
1422
1473
  expectMatch(schema, Object.assign(Object.create({ extra: 1 }), { a: "x" }));
@@ -1637,3 +1688,172 @@ test("strict is per-call, so a supported schema compiles either way", () => {
1637
1688
  invalid(aot, { a: 1, b: 1 });
1638
1689
  }
1639
1690
  });
1691
+
1692
+ test("compiled records walk own enumerable keys without Reflect.ownKeys", () => {
1693
+ // a getOwnPropertyNames snapshot plus a per-key recheck sees the same keys as the runtime's Reflect.ownKeys walk: inherited enumerables are skipped, symbols fail a string key schema but pass a symbol one, and the numeric-string retry still applies
1694
+ const sym = Symbol("s");
1695
+ const bare = compile(z.record(z.string(), z.number()));
1696
+ const inherited = Object.assign(Object.create({ proto: 1 }), { a: 1 });
1697
+ expect(bare.parse(inherited)).toEqual({ a: 1 });
1698
+ const symbolic = { a: 1, [sym]: 2 };
1699
+ expect(bare.safeParse(symbolic).error!.issues).toEqual(
1700
+ z.record(z.string(), z.number()).safeParse(symbolic).error!.issues
1701
+ );
1702
+ expect(bare.safeParse({ a: 1, b: "x" }).error!.issues).toEqual(
1703
+ z.record(z.string(), z.number()).safeParse({ a: 1, b: "x" }).error!.issues
1704
+ );
1705
+ const symKeys = compile(z.record(z.symbol(), z.number()));
1706
+ expect(symKeys.parse({ [sym]: 2 })).toEqual({ [sym]: 2 });
1707
+ expect(symKeys.safeParse({ a: 1 }).success).toBe(false);
1708
+ const emailKeys = compile(z.record(z.email(), z.number()));
1709
+ expect(emailKeys.parse({ "a@b.co": 1 })).toEqual({ "a@b.co": 1 });
1710
+ expect(emailKeys.safeParse({ "a@b.co": 1, [sym]: 2 }).success).toBe(false);
1711
+ expect(compile(z.record(z.number(), z.string())).parse({ 1: "a" })).toEqual({ 1: "a" });
1712
+ // keys are snapshotted before any value is read and rechecked when visited, like the runtime's Reflect.ownKeys walk: a getter that adds a key mid-walk is not visited, one that hides a later key skips it
1713
+ const mutating = (effect: (o: Record<PropertyKey, unknown>) => void) => {
1714
+ const o: Record<PropertyKey, unknown> = {};
1715
+ Object.defineProperty(o, "a", {
1716
+ enumerable: true,
1717
+ get() {
1718
+ effect(o);
1719
+ return 1;
1720
+ },
1721
+ });
1722
+ o.b = 2;
1723
+ return o;
1724
+ };
1725
+ const both = z.record(z.union([z.string(), z.symbol()]), z.number());
1726
+ for (const [schema, effect] of [
1727
+ [
1728
+ both,
1729
+ (o: Record<PropertyKey, unknown>) => {
1730
+ o[sym] = 2;
1731
+ },
1732
+ ],
1733
+ [
1734
+ z.record(z.string(), z.number()),
1735
+ (o: Record<PropertyKey, unknown>) => {
1736
+ o[sym] = "bad";
1737
+ },
1738
+ ],
1739
+ [
1740
+ z.record(z.string(), z.number()),
1741
+ (o: Record<PropertyKey, unknown>) => {
1742
+ o.z = "bad";
1743
+ },
1744
+ ],
1745
+ [
1746
+ z.record(z.string(), z.number()),
1747
+ (o: Record<PropertyKey, unknown>) => Object.defineProperty(o, "b", { enumerable: false }),
1748
+ ],
1749
+ ] as const) {
1750
+ const expected = schema.safeParse(mutating(effect));
1751
+ const actual = compile(schema).safeParse(mutating(effect));
1752
+ expect(actual.success).toBe(expected.success);
1753
+ if (expected.success)
1754
+ expect(Reflect.ownKeys(actual.data as object)).toEqual(Reflect.ownKeys(expected.data as object));
1755
+ else expect(actual.error!.issues).toEqual(expected.error!.issues);
1756
+ }
1757
+ // a getter that defines an own key under an inherited enumerable name mid-walk: the snapshot predates it, so neither walk visits it, and a union that accepts the record cannot be corrected by a fallback
1758
+ const shadowing = () => {
1759
+ const o = Object.create({ z: 0 });
1760
+ Object.defineProperty(o, "a", {
1761
+ enumerable: true,
1762
+ get() {
1763
+ Object.defineProperty(o, "z", { value: 2, enumerable: true });
1764
+ return 1;
1765
+ },
1766
+ });
1767
+ return o;
1768
+ };
1769
+ const inUnion = z.union([z.record(z.string(), z.number()), z.any()]);
1770
+ expect(compile(inUnion).parse(shadowing())).toStrictEqual(inUnion.parse(shadowing()));
1771
+ expect(compile(inUnion).parse(shadowing())).toStrictEqual({ a: 1 });
1772
+ // a key that is non-enumerable at snapshot time and revealed by an earlier getter is in the snapshot and passes the recheck, like the runtime
1773
+ const revealing = () => {
1774
+ const o: Record<string, unknown> = {};
1775
+ Object.defineProperty(o, "a", {
1776
+ enumerable: true,
1777
+ get() {
1778
+ Object.defineProperty(o, "b", { enumerable: true });
1779
+ return 1;
1780
+ },
1781
+ });
1782
+ Object.defineProperty(o, "b", { value: 2, enumerable: false, configurable: true });
1783
+ return o;
1784
+ };
1785
+ const plain = z.record(z.string(), z.number());
1786
+ expect(compile(plain).parse(revealing())).toStrictEqual(plain.parse(revealing()));
1787
+ expect(compile(plain).parse(revealing())).toStrictEqual({ a: 1, b: 2 });
1788
+ });
1789
+
1790
+ // withParser: a parser Zod did not generate, installed on the same wrapper compile() uses.
1791
+
1792
+ // tags its output so a test fails loudly if the runtime ran instead of the supplied parser
1793
+ function taggedParser(input: unknown) {
1794
+ if (typeof input !== "object" || input === null) return INVALID;
1795
+ const o = input as Record<string, unknown>;
1796
+ if (typeof o.a !== "string") return INVALID;
1797
+ return { a: o.a, tag: "supplied" };
1798
+ }
1799
+
1800
+ const withParserSchema = z.object({ a: z.string() });
1801
+
1802
+ test("withParser installs the supplied parser", () => {
1803
+ const installed = withParser(withParserSchema, taggedParser);
1804
+ expect(installed.parse({ a: "x" })).toStrictEqual({ a: "x", tag: "supplied" });
1805
+ // the clone is a new schema; the original keeps the runtime
1806
+ expect(installed).not.toBe(withParserSchema);
1807
+ expect(withParserSchema.parse({ a: "x" })).toStrictEqual({ a: "x" });
1808
+ });
1809
+
1810
+ test("withParser hands INVALID to the runtime and gets a real ZodError", () => {
1811
+ const installed = withParser(withParserSchema, taggedParser);
1812
+ const result = installed.safeParse({ a: 1 });
1813
+ expect(result.success).toBe(false);
1814
+ expect(result.error!.issues).toEqual(withParserSchema.safeParse({ a: 1 }).error!.issues);
1815
+ });
1816
+
1817
+ test("withParser answers validate with the supplied parser", () => {
1818
+ const installed = withParser(withParserSchema, taggedParser);
1819
+ expect(z.validate(installed, { a: "x" })).toBe(true);
1820
+ expect(z.validate(installed, { a: 1 })).toBe(false);
1821
+ });
1822
+
1823
+ test("withParser bypasses the fast path on async, like compile()", async () => {
1824
+ const installed = withParser(withParserSchema, taggedParser);
1825
+ // async runs on the runtime, so the tag is absent
1826
+ await expect(installed.parseAsync({ a: "x" })).resolves.toStrictEqual({ a: "x" });
1827
+ });
1828
+
1829
+ test("withParser reaches a parse it does not own the call site of", () => {
1830
+ const installed = withParser(withParserSchema, taggedParser);
1831
+ // installing on _zod.run rather than wrapping a call site is the whole point: a framework holding the schema gets the supplied parser through Standard Schema, and so does z.parse
1832
+ expect((installed["~standard"].validate({ a: "x" }) as { value: unknown }).value).toStrictEqual({
1833
+ a: "x",
1834
+ tag: "supplied",
1835
+ });
1836
+ expect(z.parse(installed, { a: "x" })).toStrictEqual({ a: "x", tag: "supplied" });
1837
+ });
1838
+
1839
+ test("withParser refuses a recursive schema", () => {
1840
+ type Node = { next: Node | null };
1841
+ const Node: z.ZodType<Node> = z.object({ next: z.lazy(() => z.nullable(Node)) });
1842
+ expect(() => withParser(Node, () => INVALID)).toThrow(ZodCompileUnsupportedError);
1843
+ });
1844
+
1845
+ test("withParser leaves encode on the runtime", () => {
1846
+ const codec = z.codec(z.string(), z.number(), {
1847
+ decode: (s) => Number(s),
1848
+ encode: (n) => String(n),
1849
+ });
1850
+ const installed = withParser(codec, () => INVALID);
1851
+ expect(z.encode(installed, 5)).toBe("5");
1852
+ });
1853
+
1854
+ test("withParser rejects a parser whose output contradicts the schema", () => {
1855
+ // the returned schema still claims T, so a parser that succeeds with something else would make parse() lie about its own type
1856
+ // @ts-expect-error a number is not the output of z.string()
1857
+ withParser(z.string(), () => 123);
1858
+ expectTypeOf(withParser(z.string(), () => "ok")).toEqualTypeOf<z.ZodString>();
1859
+ });
@@ -0,0 +1,140 @@
1
+ import { expect, test } from "vitest";
2
+
3
+ import { regexes } from "zod/v4/core";
4
+
5
+ // the lookahead form `regexes.email` had before this rewrite
6
+ const legacy = /^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/;
7
+
8
+ const table: [string, boolean][] = [
9
+ ["a@b.cc", true],
10
+ ["first.last@example.com", true],
11
+ ["a.b.c.d@b.cc", true],
12
+ ["'a@b.cc", true],
13
+ ["a'b@b.cc", true],
14
+ ["a+b@b.cc", true],
15
+ ["a-@b.cc", true],
16
+ ["a_@b.cc", true],
17
+ ["a@0b.cc", true],
18
+ ["a@b.c.dd", true],
19
+ ["a@b-.cc", true],
20
+ [".a@b.cc", false],
21
+ ["a.@b.cc", false],
22
+ ["a..b@b.cc", false],
23
+ ["....@b.cc", false],
24
+ ["a@b..cc", false],
25
+ ["a@.b.cc", false],
26
+ ["a@b.cc.", false],
27
+ ["a'@b.cc", false],
28
+ ["'@b.cc", false],
29
+ ["@b.cc", false],
30
+ ["a@b.c", false],
31
+ ["a@b.c0", false],
32
+ ["a@-b.cc", false],
33
+ ["a@b.cc@d.ee", false],
34
+ ["a b@b.cc", false],
35
+ ["café@b.cc", false],
36
+ ["😀@b.cc", false],
37
+ ["a\0@b.cc", false],
38
+ // `$` is not multiline, so no trailing terminator is tolerated
39
+ ["a@b.cc\n", false],
40
+ ["a@b.cc\r", false],
41
+ ["\na@b.cc", false],
42
+ ];
43
+
44
+ test("boundary cases", () => {
45
+ for (const [input, expected] of table) {
46
+ expect([input, regexes.email.test(input)]).toEqual([input, expected]);
47
+ expect([input, legacy.test(input)]).toEqual([input, expected]);
48
+ }
49
+ });
50
+
51
+ // one representative per character class the two grammars can tell apart
52
+ const classes = ["a", "0", "_", "'", "-", ".", "@", "!", "\n"];
53
+
54
+ test("exhaustive agreement over every string of length <= 6 in the class alphabet", () => {
55
+ const idx: number[] = [];
56
+ let accepted = 0;
57
+ for (let len = 0; len <= 6; len++) {
58
+ idx.length = len;
59
+ idx.fill(0);
60
+ for (;;) {
61
+ let s = "";
62
+ for (let i = 0; i < len; i++) s += classes[idx[i]!];
63
+ const a = legacy.test(s);
64
+ if (a !== regexes.email.test(s)) expect.unreachable(`disagreement on ${JSON.stringify(s)}`);
65
+ if (a) accepted++;
66
+ let p = len - 1;
67
+ while (p >= 0 && ++idx[p]! === classes.length) idx[p--] = 0;
68
+ if (p < 0) break;
69
+ }
70
+ }
71
+ // 4 chars can precede `@`, 2 can open a domain label, 1 is a letter for the TLD
72
+ expect(accepted).toBe(8);
73
+ });
74
+
75
+ function rng(seed: number) {
76
+ let s = seed >>> 0;
77
+ return () => {
78
+ s = (s + 0x6d2b79f5) >>> 0;
79
+ let t = s;
80
+ t = Math.imul(t ^ (t >>> 15), t | 1);
81
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
82
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
83
+ };
84
+ }
85
+
86
+ const local = "abZ09_'+-.".split("");
87
+ const domain = "abZ09-.".split("");
88
+ const hostile = ["@", ".", "..", "...", "\n", "\r", "\0", "\t", " ", '"', "é", "😀", "\\", "<", ";", "/", "{"];
89
+ const any = [...local, ...domain, ...hostile];
90
+
91
+ function fuzz(r: () => number): string {
92
+ const pick = <T>(xs: T[]): T => xs[Math.floor(r() * xs.length)]!;
93
+ const run = (xs: string[], max: number) => {
94
+ let out = "";
95
+ for (let i = Math.floor(r() * max); i > 0; i--) out += pick(xs);
96
+ return out;
97
+ };
98
+ switch (Math.floor(r() * 8)) {
99
+ case 0:
100
+ return run(any, 40);
101
+ case 1:
102
+ return `${run(local, 24)}@${run(domain, 24)}`;
103
+ case 2:
104
+ return `${run(local, 8)}${".".repeat(1 + Math.floor(r() * 6))}${run(local, 8)}@${run(domain, 12)}`;
105
+ case 3:
106
+ return `${run(local, 8)}@${run(domain, 8)}${".".repeat(1 + Math.floor(r() * 6))}${run(domain, 8)}`;
107
+ case 4: {
108
+ // single-character mutation of a valid address
109
+ const base = `${run("abZ09_'+-".split(""), 10)}@example.com`;
110
+ const i = Math.floor(r() * base.length);
111
+ return Math.floor(r() * 3) === 0
112
+ ? base.slice(0, i) + pick(any) + base.slice(i)
113
+ : base.slice(0, i) + (Math.floor(r() * 2) ? pick(any) : "") + base.slice(i + 1);
114
+ }
115
+ case 5:
116
+ return (
117
+ "a".repeat(1 + Math.floor(r() * 60)) +
118
+ ".".repeat(Math.floor(r() * 4)) +
119
+ run(local, 4) +
120
+ (r() < 0.5 ? "@" : "") +
121
+ run(domain, 10)
122
+ );
123
+ case 6:
124
+ return `${run(local, 8)}@${run(domain, 6)}@${run(domain, 8)}`;
125
+ default:
126
+ return pick(["", "\n", " "]) + run(local, 8) + "@example.com" + pick(["", "\n", "\r\n", " ", "\0"]);
127
+ }
128
+ }
129
+
130
+ test("differential fuzz", () => {
131
+ const r = rng(0x5eed);
132
+ let accepted = 0;
133
+ for (let i = 0; i < 25_000; i++) {
134
+ const s = fuzz(r);
135
+ const a = legacy.test(s);
136
+ if (a !== regexes.email.test(s)) expect.unreachable(`disagreement on ${JSON.stringify(s)}`);
137
+ if (a) accepted++;
138
+ }
139
+ expect(accepted).toBeGreaterThan(500);
140
+ });
@@ -0,0 +1,46 @@
1
+ import * as path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { build, transform } from "esbuild";
4
+ import { expect, test } from "vitest";
5
+ import * as core from "zod/v4/core";
6
+
7
+ // Browser polyfills for node globals land as raw text in the bundle's top scope — an esbuild `banner`, a scope-unaware injector — so the bundler never sees the collision and the browser is the first thing to read both declarations. zod must therefore declare none of these names at module scope. The JSON Schema pass shipped an `export function process` for three minors before #6397 reported the white screen it caused.
8
+ const INJECTED = ["process", "Buffer", "global", "__dirname", "__filename"];
9
+
10
+ const here = path.dirname(fileURLToPath(import.meta.url));
11
+ const CLASSIC = path.resolve(here, "../../../index.ts");
12
+ const MINI = path.resolve(here, "../../../mini/index.ts");
13
+
14
+ const polyfills = INJECTED.map((name) => `const ${name} = {};`).join("\n");
15
+
16
+ // toJSONSchema has to be reachable: a bundle that only parses a schema tree-shakes the offending module away and would pass no matter what it declares. `z.core.process` is the pre-rename export name, read as a namespace property so the entry itself declares no `process` binding.
17
+ const ENTRY = `import * as z from "%ENTRY%";\nglobalThis.__out = [z.toJSONSchema(z.string()), z.core.process];`;
18
+
19
+ test.each([
20
+ ["classic", CLASSIC],
21
+ ["mini", MINI],
22
+ ])(
23
+ "a %s bundle reaching toJSONSchema parses beside polyfilled node globals",
24
+ async (_flavor, entrypoint) => {
25
+ const result = await build({
26
+ stdin: { contents: ENTRY.replace("%ENTRY%", entrypoint), loader: "ts", resolveDir: here },
27
+ bundle: true,
28
+ format: "esm",
29
+ target: "es2020",
30
+ write: false,
31
+ logLevel: "silent",
32
+ banner: { js: polyfills },
33
+ });
34
+ const out = result.outputFiles[0]!.text;
35
+
36
+ expect(out).toContain("https://json-schema.org/draft/2020-12/schema");
37
+
38
+ // the banner is appended after esbuild has already renamed its own symbols, so this second pass is where a duplicate surfaces: `The symbol "process" has already been declared`
39
+ await expect(transform(out, { loader: "js", format: "esm" })).resolves.toBeTruthy();
40
+ },
41
+ 30000
42
+ );
43
+
44
+ test("the pre-rename `process` export still resolves to the helper", () => {
45
+ expect(core.process).toBe(core.processSchema);
46
+ });
@@ -128,7 +128,7 @@ export interface ToJSONSchemaContext {
128
128
  * covers both passes in `finalize`: the ref flattening and the `$defs` build.
129
129
  *
130
130
  * The passes are valid only while nothing they read has changed, so both are cleared in
131
- * `process()` when the map grows, and in `JSONSchemaGenerator.emit()`, which can also change
131
+ * `processSchema()` when the map grows, and in `JSONSchemaGenerator.emit()`, which can also change
132
132
  * the `cycles` and `reused` they branch on.
133
133
  *
134
134
  * One case is deliberately not covered: an `override` callback that writes to
@@ -209,7 +209,8 @@ export function handleUnrepresentable(
209
209
  return true;
210
210
  }
211
211
 
212
- export function process<T extends schemas.$ZodType>(
212
+ // never rename this back to `process`: bundler polyfills inject a top-level `const process` that a lexical declaration of the same name collides with (#6397)
213
+ export function processSchema<T extends schemas.$ZodType>(
213
214
  schema: T,
214
215
  ctx: ToJSONSchemaContext,
215
216
  _params: ProcessParams = { path: [], schemaPath: [] }
@@ -264,7 +265,7 @@ export function process<T extends schemas.$ZodType>(
264
265
  if (parent) {
265
266
  // Also set ref if processor didn't (for inheritance)
266
267
  if (!result.ref) result.ref = parent;
267
- process(parent, ctx, params);
268
+ processSchema(parent, ctx, params);
268
269
  ctx.seen.get(parent)!.isParent = true;
269
270
  }
270
271
  }
@@ -289,6 +290,9 @@ export function process<T extends schemas.$ZodType>(
289
290
  return _result.schema;
290
291
  }
291
292
 
293
+ /** @deprecated Renamed to `processSchema`. An export alias declares no binding, so it is safe to keep. */
294
+ export { processSchema as process };
295
+
292
296
  // Escape a reference token for use in a JSON Pointer fragment (RFC 6901): `~` becomes `~0` and `/` becomes `~1`. The `~` replacement must run first.
293
297
  function encodeJSONPointerSegment(segment: string): string {
294
298
  return segment.replace(/~/g, "~0").replace(/\//g, "~1");
@@ -429,8 +433,6 @@ export function extractDefs<T extends schemas.$ZodType>(
429
433
  if (seen.count > 1) {
430
434
  if (ctx.reused === "ref") {
431
435
  extractToDef(entry);
432
- // biome-ignore lint:
433
- continue;
434
436
  }
435
437
  }
436
438
  }
@@ -755,7 +757,7 @@ export function finalize<T extends schemas.$ZodType>(
755
757
  }
756
758
  }
757
759
 
758
- function isTransforming(
760
+ export function isTransforming(
759
761
  _schema: schemas.$ZodType,
760
762
  _ctx?: {
761
763
  seen: Set<schemas.$ZodType>;
@@ -834,7 +836,7 @@ export const createToJSONSchemaMethod =
834
836
  <T extends schemas.$ZodType>(schema: T, processors: Record<string, Processor> = {}) =>
835
837
  (params?: ToJSONSchemaParams): ZodStandardJSONSchemaPayload<T> => {
836
838
  const ctx = initializeContext({ ...params, processors });
837
- process(schema, ctx);
839
+ processSchema(schema, ctx);
838
840
  extractDefs(ctx, schema);
839
841
  return finalize(ctx, schema);
840
842
  };
@@ -849,7 +851,7 @@ export const createStandardJSONSchemaMethod =
849
851
  (params?: StandardJSONSchemaMethodParams): JSONSchema.BaseSchema => {
850
852
  const { libraryOptions, target } = params ?? {};
851
853
  const ctx = initializeContext({ ...(libraryOptions ?? {}), target, io, processors });
852
- process(schema, ctx);
854
+ processSchema(schema, ctx);
853
855
  extractDefs(ctx, schema);
854
856
  return finalize(ctx, schema);
855
857
  };