zod 4.5.4 → 4.6.1

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 (359) 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/discriminated-unions.test.ts +112 -6
  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 +46 -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 +194 -82
  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 +78 -28
  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 +350 -113
  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/locales/tg.test.ts +85 -0
  49. package/src/v4/core/tests/polyfill-collision.test.ts +46 -0
  50. package/src/v4/core/to-json-schema.ts +10 -8
  51. package/src/v4/core/util.ts +222 -147
  52. package/src/v4/core/versions.ts +2 -2
  53. package/src/v4/core/visit.ts +11 -0
  54. package/src/v4/locales/ar.ts +1 -0
  55. package/src/v4/locales/az.ts +1 -0
  56. package/src/v4/locales/be.ts +1 -0
  57. package/src/v4/locales/bg.ts +1 -0
  58. package/src/v4/locales/bn.ts +1 -0
  59. package/src/v4/locales/ca.ts +1 -0
  60. package/src/v4/locales/ckb.ts +1 -0
  61. package/src/v4/locales/cs.ts +1 -0
  62. package/src/v4/locales/da.ts +1 -0
  63. package/src/v4/locales/de.ts +1 -0
  64. package/src/v4/locales/el.ts +1 -0
  65. package/src/v4/locales/en.ts +1 -0
  66. package/src/v4/locales/eo.ts +1 -0
  67. package/src/v4/locales/es.ts +1 -0
  68. package/src/v4/locales/fa.ts +1 -0
  69. package/src/v4/locales/fi.ts +1 -0
  70. package/src/v4/locales/fr-CA.ts +1 -0
  71. package/src/v4/locales/fr.ts +1 -0
  72. package/src/v4/locales/gu.ts +1 -0
  73. package/src/v4/locales/he.ts +1 -0
  74. package/src/v4/locales/hi.ts +1 -0
  75. package/src/v4/locales/hr.ts +1 -0
  76. package/src/v4/locales/hu.ts +1 -0
  77. package/src/v4/locales/hy.ts +1 -0
  78. package/src/v4/locales/id.ts +1 -0
  79. package/src/v4/locales/index.ts +1 -0
  80. package/src/v4/locales/is.ts +1 -0
  81. package/src/v4/locales/it.ts +1 -0
  82. package/src/v4/locales/ja.ts +1 -0
  83. package/src/v4/locales/ka.ts +1 -0
  84. package/src/v4/locales/km.ts +1 -0
  85. package/src/v4/locales/kn.ts +1 -0
  86. package/src/v4/locales/ko.ts +1 -0
  87. package/src/v4/locales/lt.ts +1 -0
  88. package/src/v4/locales/mk.ts +1 -0
  89. package/src/v4/locales/ms.ts +1 -0
  90. package/src/v4/locales/ne.ts +1 -0
  91. package/src/v4/locales/nl.ts +1 -0
  92. package/src/v4/locales/nn.ts +1 -0
  93. package/src/v4/locales/no.ts +1 -0
  94. package/src/v4/locales/ota.ts +1 -0
  95. package/src/v4/locales/pl.ts +1 -0
  96. package/src/v4/locales/ps.ts +1 -0
  97. package/src/v4/locales/pt-BR.ts +1 -0
  98. package/src/v4/locales/pt.ts +1 -0
  99. package/src/v4/locales/ro.ts +1 -0
  100. package/src/v4/locales/ru.ts +1 -0
  101. package/src/v4/locales/sk.ts +1 -0
  102. package/src/v4/locales/sl.ts +1 -0
  103. package/src/v4/locales/sv.ts +1 -0
  104. package/src/v4/locales/ta.ts +1 -0
  105. package/src/v4/locales/tg.ts +134 -0
  106. package/src/v4/locales/th.ts +1 -0
  107. package/src/v4/locales/tk.ts +1 -0
  108. package/src/v4/locales/tr.ts +1 -0
  109. package/src/v4/locales/uk.ts +1 -0
  110. package/src/v4/locales/ur.ts +1 -0
  111. package/src/v4/locales/uz.ts +1 -0
  112. package/src/v4/locales/vi.ts +1 -0
  113. package/src/v4/locales/yo.ts +1 -0
  114. package/src/v4/locales/zh-CN.ts +1 -0
  115. package/src/v4/locales/zh-TW.ts +1 -0
  116. package/src/v4/mini/checks.ts +0 -1
  117. package/src/v4/mini/external.ts +2 -0
  118. package/src/v4/mini/schemas.ts +38 -2
  119. package/src/v4/mini/tests/checks.test.ts +1 -1
  120. package/src/v4/mini/tests/computed.test.ts +11 -17
  121. package/src/v4/mini/tests/index.test.ts +8 -0
  122. package/src/v4/mini/tests/recursive-types.test.ts +16 -0
  123. package/src/v4/mini/tests/string.test.ts +16 -0
  124. package/v3/ZodError.cjs +19 -0
  125. package/v3/errors.cjs +19 -0
  126. package/v3/external.cjs +19 -0
  127. package/v3/helpers/enumUtil.cjs +19 -0
  128. package/v3/helpers/errorUtil.cjs +20 -1
  129. package/v3/helpers/errorUtil.js +1 -1
  130. package/v3/helpers/parseUtil.cjs +19 -0
  131. package/v3/helpers/partialUtil.cjs +19 -0
  132. package/v3/helpers/typeAliases.cjs +19 -0
  133. package/v3/helpers/util.cjs +19 -0
  134. package/v3/index.cjs +19 -0
  135. package/v3/standard-schema.cjs +19 -0
  136. package/v3/types.cjs +19 -0
  137. package/v4/classic/checks.cjs +20 -2
  138. package/v4/classic/checks.d.cts +1 -1
  139. package/v4/classic/checks.d.ts +1 -1
  140. package/v4/classic/checks.js +1 -1
  141. package/v4/classic/coerce.cjs +19 -0
  142. package/v4/classic/compat.cjs +19 -0
  143. package/v4/classic/deep-partial.cjs +19 -0
  144. package/v4/classic/errors.cjs +19 -0
  145. package/v4/classic/external.cjs +22 -1
  146. package/v4/classic/external.d.cts +1 -1
  147. package/v4/classic/external.d.ts +1 -1
  148. package/v4/classic/external.js +1 -1
  149. package/v4/classic/from-json-schema.cjs +264 -24
  150. package/v4/classic/from-json-schema.js +245 -24
  151. package/v4/classic/in-out.cjs +19 -0
  152. package/v4/classic/index.cjs +19 -0
  153. package/v4/classic/iso.cjs +19 -0
  154. package/v4/classic/parse.cjs +19 -0
  155. package/v4/classic/schemas.cjs +100 -31
  156. package/v4/classic/schemas.d.cts +17 -1
  157. package/v4/classic/schemas.d.ts +17 -1
  158. package/v4/classic/schemas.js +77 -29
  159. package/v4/core/api.cjs +37 -2
  160. package/v4/core/api.d.cts +6 -4
  161. package/v4/core/api.d.ts +6 -4
  162. package/v4/core/api.js +17 -2
  163. package/v4/core/checks.cjs +19 -96
  164. package/v4/core/checks.d.cts +1 -1
  165. package/v4/core/checks.d.ts +1 -1
  166. package/v4/core/checks.js +0 -96
  167. package/v4/core/compile.cjs +165 -66
  168. package/v4/core/compile.d.cts +18 -4
  169. package/v4/core/compile.d.ts +18 -4
  170. package/v4/core/compile.js +146 -67
  171. package/v4/core/core.cjs +19 -0
  172. package/v4/core/doc.cjs +26 -2
  173. package/v4/core/doc.js +7 -2
  174. package/v4/core/errors.cjs +21 -5
  175. package/v4/core/errors.js +2 -5
  176. package/v4/core/index.cjs +19 -0
  177. package/v4/core/json-schema-generator.cjs +20 -1
  178. package/v4/core/json-schema-generator.js +2 -2
  179. package/v4/core/json-schema-processors.cjs +215 -63
  180. package/v4/core/json-schema-processors.d.cts +17 -0
  181. package/v4/core/json-schema-processors.d.ts +17 -0
  182. package/v4/core/json-schema-processors.js +195 -64
  183. package/v4/core/json-schema.cjs +19 -0
  184. package/v4/core/memoizer.cjs +114 -23
  185. package/v4/core/memoizer.js +72 -23
  186. package/v4/core/parse.cjs +56 -16
  187. package/v4/core/parse.js +37 -16
  188. package/v4/core/regexes.cjs +29 -6
  189. package/v4/core/regexes.d.cts +2 -0
  190. package/v4/core/regexes.d.ts +2 -0
  191. package/v4/core/regexes.js +8 -4
  192. package/v4/core/registries.cjs +19 -0
  193. package/v4/core/schemas.cjs +322 -95
  194. package/v4/core/schemas.d.cts +42 -7
  195. package/v4/core/schemas.d.ts +42 -7
  196. package/v4/core/schemas.js +301 -94
  197. package/v4/core/standard-schema.cjs +19 -0
  198. package/v4/core/to-json-schema.cjs +29 -7
  199. package/v4/core/to-json-schema.d.cts +7 -3
  200. package/v4/core/to-json-schema.d.ts +7 -3
  201. package/v4/core/to-json-schema.js +8 -7
  202. package/v4/core/util.cjs +181 -143
  203. package/v4/core/util.d.cts +25 -4
  204. package/v4/core/util.d.ts +25 -4
  205. package/v4/core/util.js +160 -143
  206. package/v4/core/versions.cjs +21 -2
  207. package/v4/core/versions.d.cts +1 -1
  208. package/v4/core/versions.d.ts +1 -1
  209. package/v4/core/versions.js +2 -2
  210. package/v4/core/visit.cjs +31 -0
  211. package/v4/core/visit.js +12 -0
  212. package/v4/index.cjs +19 -0
  213. package/v4/locales/ar.cjs +1 -0
  214. package/v4/locales/ar.js +1 -0
  215. package/v4/locales/az.cjs +1 -0
  216. package/v4/locales/az.js +1 -0
  217. package/v4/locales/be.cjs +1 -0
  218. package/v4/locales/be.js +1 -0
  219. package/v4/locales/bg.cjs +1 -0
  220. package/v4/locales/bg.js +1 -0
  221. package/v4/locales/bn.cjs +1 -0
  222. package/v4/locales/bn.js +1 -0
  223. package/v4/locales/ca.cjs +1 -0
  224. package/v4/locales/ca.js +1 -0
  225. package/v4/locales/ckb.cjs +1 -0
  226. package/v4/locales/ckb.js +1 -0
  227. package/v4/locales/cs.cjs +1 -0
  228. package/v4/locales/cs.js +1 -0
  229. package/v4/locales/da.cjs +1 -0
  230. package/v4/locales/da.js +1 -0
  231. package/v4/locales/de.cjs +1 -0
  232. package/v4/locales/de.js +1 -0
  233. package/v4/locales/el.cjs +1 -0
  234. package/v4/locales/el.js +1 -0
  235. package/v4/locales/en.cjs +1 -0
  236. package/v4/locales/en.js +1 -0
  237. package/v4/locales/eo.cjs +1 -0
  238. package/v4/locales/eo.js +1 -0
  239. package/v4/locales/es.cjs +1 -0
  240. package/v4/locales/es.js +1 -0
  241. package/v4/locales/fa.cjs +1 -0
  242. package/v4/locales/fa.js +1 -0
  243. package/v4/locales/fi.cjs +1 -0
  244. package/v4/locales/fi.js +1 -0
  245. package/v4/locales/fr-CA.cjs +1 -0
  246. package/v4/locales/fr-CA.js +1 -0
  247. package/v4/locales/fr.cjs +1 -0
  248. package/v4/locales/fr.js +1 -0
  249. package/v4/locales/gu.cjs +1 -0
  250. package/v4/locales/gu.js +1 -0
  251. package/v4/locales/he.cjs +1 -0
  252. package/v4/locales/he.js +1 -0
  253. package/v4/locales/hi.cjs +1 -0
  254. package/v4/locales/hi.js +1 -0
  255. package/v4/locales/hr.cjs +1 -0
  256. package/v4/locales/hr.js +1 -0
  257. package/v4/locales/hu.cjs +1 -0
  258. package/v4/locales/hu.js +1 -0
  259. package/v4/locales/hy.cjs +1 -0
  260. package/v4/locales/hy.js +1 -0
  261. package/v4/locales/id.cjs +1 -0
  262. package/v4/locales/id.js +1 -0
  263. package/v4/locales/index.cjs +22 -1
  264. package/v4/locales/index.d.cts +1 -0
  265. package/v4/locales/index.d.ts +1 -0
  266. package/v4/locales/index.js +1 -0
  267. package/v4/locales/is.cjs +1 -0
  268. package/v4/locales/is.js +1 -0
  269. package/v4/locales/it.cjs +1 -0
  270. package/v4/locales/it.js +1 -0
  271. package/v4/locales/ja.cjs +1 -0
  272. package/v4/locales/ja.js +1 -0
  273. package/v4/locales/ka.cjs +1 -0
  274. package/v4/locales/ka.js +1 -0
  275. package/v4/locales/km.cjs +1 -0
  276. package/v4/locales/km.js +1 -0
  277. package/v4/locales/kn.cjs +1 -0
  278. package/v4/locales/kn.js +1 -0
  279. package/v4/locales/ko.cjs +1 -0
  280. package/v4/locales/ko.js +1 -0
  281. package/v4/locales/lt.cjs +1 -0
  282. package/v4/locales/lt.js +1 -0
  283. package/v4/locales/mk.cjs +1 -0
  284. package/v4/locales/mk.js +1 -0
  285. package/v4/locales/ms.cjs +1 -0
  286. package/v4/locales/ms.js +1 -0
  287. package/v4/locales/ne.cjs +1 -0
  288. package/v4/locales/ne.js +1 -0
  289. package/v4/locales/nl.cjs +1 -0
  290. package/v4/locales/nl.js +1 -0
  291. package/v4/locales/nn.cjs +1 -0
  292. package/v4/locales/nn.js +1 -0
  293. package/v4/locales/no.cjs +1 -0
  294. package/v4/locales/no.js +1 -0
  295. package/v4/locales/ota.cjs +1 -0
  296. package/v4/locales/ota.js +1 -0
  297. package/v4/locales/pl.cjs +1 -0
  298. package/v4/locales/pl.js +1 -0
  299. package/v4/locales/ps.cjs +1 -0
  300. package/v4/locales/ps.js +1 -0
  301. package/v4/locales/pt-BR.cjs +1 -0
  302. package/v4/locales/pt-BR.js +1 -0
  303. package/v4/locales/pt.cjs +1 -0
  304. package/v4/locales/pt.js +1 -0
  305. package/v4/locales/ro.cjs +1 -0
  306. package/v4/locales/ro.js +1 -0
  307. package/v4/locales/ru.cjs +1 -0
  308. package/v4/locales/ru.js +1 -0
  309. package/v4/locales/sk.cjs +1 -0
  310. package/v4/locales/sk.js +1 -0
  311. package/v4/locales/sl.cjs +1 -0
  312. package/v4/locales/sl.js +1 -0
  313. package/v4/locales/sv.cjs +1 -0
  314. package/v4/locales/sv.js +1 -0
  315. package/v4/locales/ta.cjs +1 -0
  316. package/v4/locales/ta.js +1 -0
  317. package/v4/locales/tg.cjs +143 -0
  318. package/v4/locales/tg.d.cts +5 -0
  319. package/v4/locales/tg.d.ts +4 -0
  320. package/v4/locales/tg.js +116 -0
  321. package/v4/locales/th.cjs +1 -0
  322. package/v4/locales/th.js +1 -0
  323. package/v4/locales/tk.cjs +1 -0
  324. package/v4/locales/tk.js +1 -0
  325. package/v4/locales/tr.cjs +1 -0
  326. package/v4/locales/tr.js +1 -0
  327. package/v4/locales/uk.cjs +1 -0
  328. package/v4/locales/uk.js +1 -0
  329. package/v4/locales/ur.cjs +1 -0
  330. package/v4/locales/ur.js +1 -0
  331. package/v4/locales/uz.cjs +1 -0
  332. package/v4/locales/uz.js +1 -0
  333. package/v4/locales/vi.cjs +1 -0
  334. package/v4/locales/vi.js +1 -0
  335. package/v4/locales/yo.cjs +1 -0
  336. package/v4/locales/yo.js +1 -0
  337. package/v4/locales/zh-CN.cjs +1 -0
  338. package/v4/locales/zh-CN.js +1 -0
  339. package/v4/locales/zh-TW.cjs +1 -0
  340. package/v4/locales/zh-TW.js +1 -0
  341. package/v4/mini/checks.cjs +20 -2
  342. package/v4/mini/checks.d.cts +1 -1
  343. package/v4/mini/checks.d.ts +1 -1
  344. package/v4/mini/checks.js +1 -1
  345. package/v4/mini/coerce.cjs +19 -0
  346. package/v4/mini/deep-partial.cjs +19 -0
  347. package/v4/mini/external.cjs +22 -1
  348. package/v4/mini/external.d.cts +1 -1
  349. package/v4/mini/external.d.ts +1 -1
  350. package/v4/mini/external.js +1 -1
  351. package/v4/mini/in-out.cjs +19 -0
  352. package/v4/mini/index.cjs +19 -0
  353. package/v4/mini/iso.cjs +19 -0
  354. package/v4/mini/parse.cjs +19 -0
  355. package/v4/mini/schemas.cjs +42 -4
  356. package/v4/mini/schemas.d.cts +8 -0
  357. package/v4/mini/schemas.d.ts +8 -0
  358. package/v4/mini/schemas.js +19 -2
  359. package/v4-mini/index.cjs +19 -0
@@ -1,3 +1,5 @@
1
+ import v8 from "node:v8";
2
+ import vm from "node:vm";
1
3
  import { expect, test } from "vitest";
2
4
  import * as z from "zod/v4";
3
5
 
@@ -718,3 +720,275 @@ test("detects a cycle through a user-defined container type", () => {
718
720
  const out: any = Node.parse(input);
719
721
  expect(out.boxed.v).toBe(out);
720
722
  });
723
+
724
+ // #6526: a factory returns fresh instances, so the identity walk never revisits a node; it descends until the stack overflows unless it stops at the deferred edge
725
+ test("parses a factory-built recursive schema", () => {
726
+ const Node = (): any =>
727
+ z.object({
728
+ id: z.number(),
729
+ get kids() {
730
+ return z.array(Node());
731
+ },
732
+ });
733
+
734
+ const S = z.object({ root: Node() });
735
+ const out = S.parse({ root: { id: 1, kids: [{ id: 2, kids: [] }] } });
736
+ expect(out.root.kids[0].id).toBe(2);
737
+ expect(S.safeParse({ root: { id: 1, kids: [{ id: "x", kids: [] }] } }).success).toBe(false);
738
+ });
739
+
740
+ test("parses a factory-built recursive discriminated union at the root and nested", () => {
741
+ const Geometry = (): any =>
742
+ z.discriminatedUnion("type", [
743
+ z.object({ type: z.literal("Point"), coordinates: z.array(z.number()) }),
744
+ z.looseObject({
745
+ type: z.literal("GeometryCollection"),
746
+ get geometries() {
747
+ return z.array(Geometry());
748
+ },
749
+ }),
750
+ ]);
751
+
752
+ const collection = { type: "GeometryCollection", geometries: [{ type: "Point", coordinates: [1, 2] }] };
753
+ expect(Geometry().parse(collection).geometries[0].coordinates).toEqual([1, 2]);
754
+ expect(z.object({ g: Geometry() }).parse({ g: collection }).g.type).toBe("GeometryCollection");
755
+ });
756
+
757
+ test("the walk reports a cycle for a deferred edge instead of resolving it", () => {
758
+ const Node = (): any =>
759
+ z.object({
760
+ get self() {
761
+ return Node();
762
+ },
763
+ });
764
+ expect(z.core.isRecursiveSchema(Node())).toBe(true);
765
+
766
+ // conservative by design: a getter that closes no cycle is reported too, and pays only the memoizer it keeps
767
+ const Leaf = z.object({ id: z.string() });
768
+ const forward: any = z.object({
769
+ get leaf() {
770
+ return Leaf;
771
+ },
772
+ });
773
+ expect(z.core.isRecursiveSchema(forward)).toBe(true);
774
+
775
+ // a lazy is followed one hop, which is what lets z.compile see past it; past that hop it is deferred like any other edge
776
+ expect(z.core.isRecursiveSchema(z.lazy(() => Leaf) as any)).toBe(false);
777
+ expect(z.core.isRecursiveSchema(z.lazy(() => z.lazy(() => Leaf)) as any)).toBe(true);
778
+ });
779
+
780
+ // the builders answer `shape` from an accessor that resolves the source's getters, so the walk has to reach the source schema itself
781
+ test("parses a factory-built recursive schema through every object builder", () => {
782
+ const wraps: [string, (base: () => any) => any][] = [
783
+ ["extend", (b) => b().extend({ x: z.string() })],
784
+ ["safeExtend", (b) => b().safeExtend({ x: z.string() })],
785
+ ["merge", (b) => z.object({ x: z.string() }).merge(b())],
786
+ ["partial", (b) => b().partial()],
787
+ ["required", (b) => b().required()],
788
+ ["pick", (b) => b().pick({ self: true })],
789
+ ["omit", (b) => b().omit({ y: true })],
790
+ ["chained", (b) => b().extend({ x: z.string() }).partial()],
791
+ // rebuilds the def without touching the shape, so the clone carries the source's accessor rather than a shape of its own
792
+ ["strict", (b) => b().extend({ x: z.string() }).strict()],
793
+ ["catchall", (b) => b().extend({ x: z.string() }).catchall(z.unknown())],
794
+ ["meta", (b) => b().extend({ x: z.string() }).meta({ id: "node" })],
795
+ ];
796
+
797
+ for (const [name, wrap] of wraps) {
798
+ const Node = (): any =>
799
+ wrap(() =>
800
+ z.object({
801
+ y: z.string(),
802
+ get self() {
803
+ return z.optional(Node());
804
+ },
805
+ })
806
+ );
807
+ const S = z.object({ r: Node() });
808
+ for (const attempt of ["first", "second"]) {
809
+ expect(() => S.safeParse({ r: { y: "s", x: "s" } }), `${name} (${attempt} parse)`).not.toThrow();
810
+ }
811
+ }
812
+ });
813
+
814
+ test("an ordinary builder stays exact, so it still compiles", () => {
815
+ const plain = z.object({ a: z.string(), b: z.number() });
816
+ const derived: [string, any][] = [
817
+ ["extend", plain.extend({ c: z.boolean() })],
818
+ ["merge", plain.merge(z.object({ d: z.string() }))],
819
+ ["partial", plain.partial()],
820
+ ["required", plain.partial().required()],
821
+ ["pick", plain.pick({ a: true })],
822
+ ["omit", plain.omit({ a: true })],
823
+ ["strict", plain.extend({ c: z.boolean() }).strict()],
824
+ ["catchall", plain.extend({ c: z.boolean() }).catchall(z.unknown())],
825
+ ["meta", plain.extend({ c: z.boolean() }).meta({ id: "plain" })],
826
+ ];
827
+ for (const [name, schema] of derived) {
828
+ expect(z.core.isRecursiveSchema(schema), name).toBe(false);
829
+ expect(() => z.compile(schema, { strict: true }), name).not.toThrow();
830
+ }
831
+ });
832
+
833
+ // a derived shape mirrors its source's descriptors, so dropping or overwriting the key that carries the cycle leaves nothing deferred and the walk is exact before anything parses
834
+ test("a builder that drops the recursive key ends up exact", () => {
835
+ const Node: any = z.object({
836
+ id: z.number(),
837
+ get self() {
838
+ return z.optional(Node);
839
+ },
840
+ });
841
+
842
+ const derived: [string, any, any][] = [
843
+ ["omit", Node.omit({ self: true }), { id: 1 }],
844
+ ["pick", Node.pick({ id: true }), { id: 1 }],
845
+ ["extend overwrites it", Node.extend({ self: z.string() }), { id: 1, self: "x" }],
846
+ ];
847
+ for (const [name, schema, input] of derived) {
848
+ expect(z.core.isRecursiveSchema(schema), name).toBe(false);
849
+ expect(() => z.compile(schema, { strict: true }), name).not.toThrow();
850
+ expect(schema.parse(input), name).toEqual(input);
851
+ }
852
+
853
+ // the branch that stays is still found
854
+ const kept = Node.extend({ x: z.string() });
855
+ expect(z.core.isRecursiveSchema(kept)).toBe(true);
856
+ });
857
+
858
+ // resolving a lazy runs user code, which can re-enter the walk; a walk that kept its certainty outside its own frames would have the inner one clobber the outer
859
+ test("a nested walk can't corrupt the one that resolved into it", () => {
860
+ const Node: any = z.object({
861
+ id: z.number(),
862
+ self: z.lazy(() => {
863
+ z.compile(z.string(), { strict: true });
864
+ return z.optional(Node);
865
+ }),
866
+ });
867
+
868
+ const omitted = Node.omit({ self: true });
869
+ // exact before anything parses: the mirrored shape kept only `id`, so nothing here is deferred
870
+ expect(z.core.isRecursiveSchema(omitted)).toBe(false);
871
+ omitted.parse({ id: 1 });
872
+ expect(z.core.isRecursiveSchema(omitted)).toBe(false);
873
+ expect(() => z.compile(omitted, { strict: true })).not.toThrow();
874
+
875
+ const shared = { id: 1 };
876
+ const out: any = z.object({ a: omitted, b: omitted }).parse({ a: shared, b: shared });
877
+ expect(out.a).not.toBe(out.b);
878
+ expect(z.core.isRecursiveSchema(Node)).toBe(true);
879
+ });
880
+
881
+ test("detects a cycle that closes through a builder", () => {
882
+ const Node: any = z
883
+ .object({
884
+ id: z.number(),
885
+ get self() {
886
+ return z.optional(Node);
887
+ },
888
+ })
889
+ .extend({ x: z.string() });
890
+
891
+ const input: any = { id: 1, x: "s" };
892
+ input.self = input;
893
+ const out = Node.parse(input);
894
+ expect(out.self).toBe(out);
895
+ });
896
+
897
+ test("a deferred edge stops being assumed a cycle once the graph resolves", () => {
898
+ const Leaf = z.object({ n: z.number() });
899
+ const Forward: any = z.object({
900
+ get a() {
901
+ return Leaf;
902
+ },
903
+ });
904
+ expect(z.core.isRecursiveSchema(Forward)).toBe(true);
905
+ Forward.parse({ a: { n: 1 } });
906
+ // the parse resolved the getter, so the walk can follow it and drop the assumption
907
+ expect(z.core.isRecursiveSchema(Forward)).toBe(false);
908
+
909
+ // a factory hands back a fresh subtree every time, so no parse ever resolves it into a finite graph
910
+ const Node = (): any =>
911
+ z.object({
912
+ get kids() {
913
+ return z.array(Node());
914
+ },
915
+ });
916
+ const Generated = Node();
917
+ expect(z.core.isRecursiveSchema(Generated)).toBe(true);
918
+ Generated.parse({ kids: [] });
919
+ expect(z.core.isRecursiveSchema(Generated)).toBe(true);
920
+
921
+ // a lazy that closes a real cycle stays recursive however often it resolves
922
+ const Cyclic: any = z.object({ id: z.number(), self: z.lazy(() => z.optional(Cyclic)) });
923
+ Cyclic.parse({ id: 1 });
924
+ expect(z.core.isRecursiveSchema(Cyclic)).toBe(true);
925
+ });
926
+
927
+ // `z.object` resolves its shape by spread, so a non-enumerable key is no part of it; `z.properties` reads every own key, so one there is still parsed and can still close a cycle
928
+ test("a non-enumerable key counts for z.properties and not for z.object", () => {
929
+ const props = (enumerable: boolean) => {
930
+ const shape: Record<string, any> = { id: z.number() };
931
+ const P = z.properties(shape);
932
+ Object.defineProperty(shape, "next", { value: z.lazy(() => z.optional(P)), enumerable, configurable: true });
933
+ return P;
934
+ };
935
+ expect(z.core.isRecursiveSchema(props(true) as any)).toBe(true);
936
+ expect(z.core.isRecursiveSchema(props(false) as any)).toBe(true);
937
+
938
+ const object = (enumerable: boolean) => {
939
+ const shape: Record<string, any> = { id: z.number() };
940
+ const O: any = z.object(shape);
941
+ Object.defineProperty(shape, "next", { value: z.lazy(() => z.optional(O)), enumerable, configurable: true });
942
+ return O;
943
+ };
944
+ expect(z.core.isRecursiveSchema(object(true))).toBe(true);
945
+ // the spread drops it, so nothing can ever parse it
946
+ expect(z.core.isRecursiveSchema(object(false))).toBe(false);
947
+ });
948
+
949
+ test("the walk stays exact where nothing is deferred", () => {
950
+ let deep: any = z.string();
951
+ for (let i = 0; i < 300; i++) deep = z.object({ v: deep });
952
+ expect(z.core.isRecursiveSchema(deep)).toBe(false);
953
+ });
954
+
955
+ test("parses a factory-built recursive schema through z.lazy", () => {
956
+ const Node = (): any =>
957
+ z.object({
958
+ id: z.number(),
959
+ child: z.lazy(() => z.optional(Node())),
960
+ });
961
+
962
+ const out = z.object({ root: Node() }).parse({ root: { id: 1, child: { id: 2, child: undefined } } });
963
+ expect(out.root.child.id).toBe(2);
964
+ });
965
+
966
+ test("a finished parse pins nothing on the schema", async () => {
967
+ // es2020 is the target, and its lib predates WeakRef
968
+ type Weak<T extends object> = { deref(): T | undefined };
969
+ const { WeakRef: Weak } = globalThis as unknown as { WeakRef: new <T extends object>(target: T) => Weak<T> };
970
+ // vitest carries no --expose-gc, so reach the collector the way node's own tests do
971
+ v8.setFlagsFromString("--expose-gc");
972
+ const gc = vm.runInNewContext("gc") as () => void;
973
+
974
+ const Node: any = z.object({
975
+ id: z.number(),
976
+ get next() {
977
+ return z.optional(Node);
978
+ },
979
+ });
980
+
981
+ const ref = ((): Weak<object> => {
982
+ const input = { id: 1, next: { id: 2, next: undefined } };
983
+ Node.parse(input);
984
+ return new Weak(input);
985
+ })();
986
+
987
+ for (let attempt = 0; attempt < 10 && ref.deref(); attempt++) {
988
+ // the input stays on the stack until a macrotask boundary
989
+ await new Promise((resolve) => setTimeout(resolve, 0));
990
+ gc();
991
+ }
992
+
993
+ expect(ref.deref()).toBeUndefined();
994
+ });
@@ -740,6 +740,26 @@ test("encode with codec discriminator", () => {
740
740
  expect(encoded).toEqual({ type: 1, value: "hello" });
741
741
  });
742
742
 
743
+ test("nested encoding does not select a sibling from forward discriminator values", async () => {
744
+ const tag = (value: "a" | "b") =>
745
+ z.codec(z.literal(value).default(value), z.undefined(), {
746
+ decode: () => undefined,
747
+ encode: () => value,
748
+ });
749
+ const inner = z.discriminatedUnion("type", [
750
+ z.object({ type: tag("a"), value: z.literal("a") }),
751
+ z.object({ type: tag("b"), value: z.literal("b") }),
752
+ ]);
753
+ const outer = z.discriminatedUnion("type", [inner, z.object({ type: z.undefined(), value: z.string() })]);
754
+ for (const value of ["a", "b"] as const) {
755
+ const input = { type: undefined, value };
756
+ const expected = { type: value, value };
757
+ expect(z.encode(inner, input)).toEqual(expected);
758
+ expect(z.encode(outer, input)).toEqual(expected);
759
+ expect(await z.encodeAsync(outer, input)).toEqual(expected);
760
+ }
761
+ });
762
+
743
763
  test("getDiscriminatedOption", () => {
744
764
  const fruit = z.object({ type: z.literal("fruit"), seeds: z.boolean() });
745
765
  const veg = z.object({ type: z.literal("vegetable"), leafy: z.boolean() });
@@ -811,9 +831,14 @@ test.each(["__proto__", "constructor", "toString", "hasOwnProperty", "valueOf"])
811
831
  }
812
832
  );
813
833
 
814
- // An omittable discriminator reads back as undefined at the lookup, exactly as TypeScript sees it: `{ k?: "a" } | { k?: "c" }` does not narrow on `k === undefined`.
815
834
  test("an omittable discriminator claims undefined", () => {
816
- const omittable = [z.exactOptional(z.literal("a")), z.optional(z.literal("a")), z.literal("a").default("a")];
835
+ const omittable = [
836
+ z.exactOptional(z.literal("a")),
837
+ z.optional(z.literal("a")),
838
+ z.literal("a").default("a"),
839
+ z.literal("a").prefault("a"),
840
+ z.literal("a").catch("a"),
841
+ ];
817
842
  for (const k of omittable) {
818
843
  expect(z.object({ k })._zod.propValues.k).toEqual(new Set(["a", undefined]));
819
844
  }
@@ -827,10 +852,91 @@ test("an omittable discriminator claims undefined", () => {
827
852
  expect(z.union(options).safeParse({ x: "s" }).success).toEqual(true);
828
853
  expect(z.discriminatedUnion("k", options).safeParse({ k: "b", y: 1 }).success).toEqual(true);
829
854
 
830
- // two options omit the key: both claim undefined, so they are not discriminable on it
855
+ // ambiguous absence does not prevent explicit tags from routing
831
856
  for (const k of omittable) {
832
- expect(() =>
833
- z.discriminatedUnion("k", [z.object({ k }), z.object({ k: z.exactOptional(z.literal("c")) })]).parse({})
834
- ).toThrow(/Duplicate discriminator value "undefined"/);
857
+ const schema = z.discriminatedUnion("k", [z.object({ k }), z.object({ k: z.exactOptional(z.literal("c")) })]);
858
+ expect(schema.parse({ k: "a" })).toEqual({ k: "a" });
859
+ expect(schema.parse({ k: "c" })).toEqual({ k: "c" });
860
+ expect(schema.safeParse({}).success).toBe(false);
861
+ }
862
+ });
863
+
864
+ test("defaulted discriminators preserve tagged parsing without guessing a member", async () => {
865
+ const a = z.object({ type: z.literal("a").default("a"), x: z.number().positive() });
866
+ const b = z.object({ type: z.literal("b").default("b"), y: z.string() });
867
+ const c = z.object({ type: z.literal("c").default("c"), z: z.boolean() });
868
+ for (const options of [
869
+ [a, b, c],
870
+ [c, b, a],
871
+ ] as const) {
872
+ const schema = z.discriminatedUnion("type", options);
873
+ for (const input of [a.parse({ x: 1 }), b.parse({ y: "s" }), c.parse({ z: true })]) {
874
+ expect(schema.parse(input)).toEqual(input);
875
+ expect((await schema.safeParseAsync(input)).success).toBe(true);
876
+ }
877
+ for (const input of [{ x: 1, y: "s", z: true }, { type: undefined }, { type: "other" }, { type: "a", x: -1 }]) {
878
+ expect(schema.safeParse(input).success).toBe(false);
879
+ expect((await schema.safeParseAsync(input)).success).toBe(false);
880
+ }
881
+ const result = schema.safeParse({});
882
+ expect(result.error?.issues[0]).toMatchObject({
883
+ code: "invalid_union",
884
+ path: ["type"],
885
+ options: options.map((o) => o.shape.type.unwrap().value),
886
+ });
887
+ expect(z.getDiscriminatedOption(schema, "a")).toBe(a);
888
+ }
889
+ const unique = z.discriminatedUnion("type", [a, b.safeExtend({ type: b.shape.type.unwrap() })]);
890
+ expect(unique.parse({ x: 1 })).toEqual({ type: "a", x: 1 });
891
+ });
892
+
893
+ test("undefined collisions are value-based and discriminator lookup rejects ambiguity", () => {
894
+ const a = z.object({ type: z.literal("a").optional() });
895
+ const absent = z.object({ type: z.undefined() });
896
+ for (const options of [
897
+ [a, absent],
898
+ [absent, a],
899
+ [absent, absent, a],
900
+ ] as const) {
901
+ const schema = z.discriminatedUnion("type", options);
902
+ expect(schema.parse({ type: "a" })).toEqual({ type: "a" });
903
+ expect(schema.safeParse({}).success).toBe(false);
904
+ expect(() => z.getDiscriminatedOption(schema, undefined)).toThrow('Ambiguous discriminator value "undefined"');
905
+ }
906
+ const unique = z.discriminatedUnion("type", [absent, z.object({ type: z.literal("a") })]);
907
+ expect(unique.parse({ type: undefined })).toEqual({ type: undefined });
908
+ expect(z.getDiscriminatedOption(unique, undefined)).toBe(absent);
909
+ });
910
+
911
+ test("non-undefined discriminator collisions remain schema errors", () => {
912
+ for (const tag of [z.literal("a").default("a"), z.literal(["a", "b"]), z.literal("a").nullable()]) {
913
+ const schema = z.discriminatedUnion("type", [z.object({ type: tag }), z.object({ type: tag })]);
914
+ expect(() => schema.safeParse({ type: "a" })).toThrow(/Duplicate discriminator value/);
915
+ expect(() => z.encode(schema, { type: "a" })).toThrow(/Duplicate discriminator value/);
916
+ expect(() => z.getDiscriminatedOption(schema, "a")).toThrow(/Duplicate discriminator value/);
917
+ }
918
+ const nullable = z.discriminatedUnion("type", [
919
+ z.object({ type: z.literal("a").nullable() }),
920
+ z.object({ type: z.literal("b").nullable() }),
921
+ ]);
922
+ expect(() => nullable.safeParse({ type: "a" })).toThrow('Duplicate discriminator value "null"');
923
+ });
924
+
925
+ test("nested defaulted unions only advertise routable discriminator values", () => {
926
+ const a = z.object({ type: z.literal("a").default("a"), group: z.literal("inner") });
927
+ const b = z.object({ type: z.literal("b").default("b"), group: z.literal("inner") });
928
+ const inner = z.discriminatedUnion("type", [a, b]);
929
+ const c = z.object({ type: z.literal("c").default("c"), group: z.literal("outer") });
930
+ expect(inner._zod.propValues.type).toEqual(new Set(["a", "b"]));
931
+ for (const schema of [
932
+ z.discriminatedUnion("type", [z.lazy(() => inner), c]),
933
+ z.discriminatedUnion("group", [inner, c]),
934
+ ]) {
935
+ expect(schema.parse({ type: "a", group: "inner" })).toEqual({ type: "a", group: "inner" });
936
+ expect(schema.parse({ group: "outer" })).toEqual({ type: "c", group: "outer" });
937
+ expect(schema.safeParse({ group: "inner" }).success).toBe(false);
835
938
  }
939
+ const fallback = z.discriminatedUnion("type", [a, b], { unionFallback: true });
940
+ expect(fallback._zod.propValues.type.has(undefined)).toBe(true);
941
+ expect(fallback.parse({ group: "inner" })).toEqual({ type: "a", group: "inner" });
836
942
  });
@@ -87,6 +87,15 @@ test("enum from non-const inputs", () => {
87
87
 
88
88
  test("get options", () => {
89
89
  expect(z.enum(["tuna", "trout"]).options).toEqual(["tuna", "trout"]);
90
+
91
+ enum Fish {
92
+ Tuna = 0,
93
+ Trout = 1,
94
+ }
95
+
96
+ // numeric enums carry reverse-mapping keys; options lists only what parse accepts
97
+ expect(z.enum(Fish).options).toEqual([Fish.Tuna, Fish.Trout]);
98
+ expect(z.enum(Fish).safeParse("Tuna").success).toEqual(false);
90
99
  });
91
100
 
92
101
  test("readonly enum", () => {
@@ -1,5 +1,7 @@
1
1
  import { inspect, types } from "node:util";
2
- import { afterEach, expect, test, vi } from "vitest";
2
+ import v8 from "node:v8";
3
+ import vm from "node:vm";
4
+ import { afterEach, describe, expect, test, vi } from "vitest";
3
5
  import * as z from "zod/v4";
4
6
 
5
7
  // Several tests install a global error map. Resetting on the last line of each leaks it into every later test in the file whenever an assertion above that line fails, which turns one real failure into a cascade.
@@ -977,3 +979,130 @@ test("error initializer never installs onto an intrinsic prototype", () => {
977
979
  expect({}.toString()).toEqual("[object Object]");
978
980
  expect(String(new Error("boom"))).toEqual("Error: boom");
979
981
  });
982
+
983
+ describe("safeParse builds the error on first read", () => {
984
+ class CountingError {
985
+ static constructed = 0;
986
+ constructor(public issues: unknown[]) {
987
+ CountingError.constructed++;
988
+ }
989
+ }
990
+
991
+ test("constructs once, on read", () => {
992
+ CountingError.constructed = 0;
993
+ const result = z.core._safeParse(CountingError as any)(z.string(), 1) as any;
994
+ expect(result.success).toBe(false);
995
+ expect(CountingError.constructed).toBe(0);
996
+ const err = result.error;
997
+ expect(CountingError.constructed).toBe(1);
998
+ expect(result.error).toBe(err);
999
+ expect(CountingError.constructed).toBe(1);
1000
+ expect(err.issues[0].code).toBe("invalid_type");
1001
+ });
1002
+
1003
+ test("async variant", async () => {
1004
+ CountingError.constructed = 0;
1005
+ const result = (await z.core._safeParseAsync(CountingError as any)(z.string(), 1)) as any;
1006
+ expect(CountingError.constructed).toBe(0);
1007
+ expect(result.error).toBeInstanceOf(CountingError);
1008
+ expect(CountingError.constructed).toBe(1);
1009
+ });
1010
+
1011
+ test("result stays plain data", () => {
1012
+ const result = z.string().safeParse(1);
1013
+ expect(Object.keys(result)).toEqual(["success", "error"]);
1014
+ expect(result).toEqual({ success: false, error: expect.any(z.ZodError) });
1015
+ expect(structuredClone(result).error).toBeInstanceOf(Error);
1016
+ const replaced = new z.ZodError([]);
1017
+ (result as any).error = replaced;
1018
+ expect(result.error).toBe(replaced);
1019
+ });
1020
+
1021
+ test("reading the error releases the parsed input", async () => {
1022
+ // es2020 is the target, and its lib predates WeakRef
1023
+ type Weak<T extends object> = { deref(): T | undefined };
1024
+ const { WeakRef: Weak } = globalThis as unknown as { WeakRef: new <T extends object>(target: T) => Weak<T> };
1025
+ // vitest carries no --expose-gc, so reach the collector the way node's own tests do
1026
+ v8.setFlagsFromString("--expose-gc");
1027
+ const gc = vm.runInNewContext("gc") as () => void;
1028
+
1029
+ const schema = z.string();
1030
+ const refs: Weak<object>[] = [];
1031
+ const results: unknown[] = [];
1032
+ for (let i = 0; i < 50; i++) {
1033
+ const input = { i, blob: new Array(500).fill(i) };
1034
+ refs.push(new Weak(input));
1035
+ const result = schema.safeParse(input);
1036
+ // half read the error, half replace it without ever reading; both have to release
1037
+ if (i % 2) void result.error;
1038
+ else (result as { error: z.core.$ZodError }).error = new z.ZodError([]);
1039
+ results.push(result);
1040
+ }
1041
+ // the collector decides when it runs, so give it several passes; a retained input never clears no matter how many it gets
1042
+ const live = () => refs.filter((ref) => ref.deref() !== undefined).length;
1043
+ for (let attempt = 0; attempt < 10 && live() > 1; attempt++) {
1044
+ // the loop's last input stays on the stack until a macrotask boundary
1045
+ await new Promise((resolve) => setTimeout(resolve, 0));
1046
+ gc();
1047
+ }
1048
+ expect(live()).toBeLessThanOrEqual(1);
1049
+ expect(results).toHaveLength(50);
1050
+ });
1051
+
1052
+ test("no reader frames leak into the error, however late it is read", async () => {
1053
+ const result = z.string().safeParse(12) as { error: z.core.$ZodError };
1054
+
1055
+ // the constructor runs at THIS read: a macrotask later and twenty frames deeper than the parse
1056
+ await new Promise((resolve) => setTimeout(resolve, 0));
1057
+ const readDeep = <T>(n: number, make: () => T): T => (n ? readDeep(n - 1, make) : make());
1058
+ const error = readDeep(20, () => result.error) as unknown as Error;
1059
+
1060
+ // control: a plain Error at that same depth does carry the frames, so the assertions below discriminate rather than pass vacuously
1061
+ const control = readDeep(20, () => new Error("control"));
1062
+ expect(control.stack!.split("\n").some((line) => line.trim().startsWith("at "))).toBe(true);
1063
+ expect(control.stack!).toContain("readDeep");
1064
+
1065
+ expect(error.stack!.startsWith("ZodError: [")).toBe(true);
1066
+ expect(error.stack!.split("\n").some((line) => line.trim().startsWith("at "))).toBe(false);
1067
+ expect(error.stack!).not.toContain("readDeep");
1068
+ });
1069
+
1070
+ test("a lazy build gives back the caller's stackTraceLimit, not the parse-time one", () => {
1071
+ const result = z.string().safeParse(12) as { error: z.core.$ZodError };
1072
+ const ambient = Error.stackTraceLimit;
1073
+ // changed between the parse and the read, so restoring a value captured at parse time would clobber it
1074
+ Error.stackTraceLimit = ambient + 7;
1075
+ try {
1076
+ void result.error;
1077
+ expect(Error.stackTraceLimit).toBe(ambient + 7);
1078
+ } finally {
1079
+ Error.stackTraceLimit = ambient;
1080
+ }
1081
+ });
1082
+ });
1083
+
1084
+ test("a finalized issue copies the raw issue's own keys only", () => {
1085
+ const proto = { inherited: true };
1086
+ const schema = z.string().check((ctx) => {
1087
+ const raw = Object.create(proto);
1088
+ Object.assign(raw, { code: "custom", message: "own", input: ctx.value, extra: 1 });
1089
+ ctx.issues.push(raw);
1090
+ });
1091
+ const issue = schema.safeParse("x").error!.issues[0];
1092
+ expect(issue).toStrictEqual({ code: "custom", message: "own", path: [], extra: 1 });
1093
+ expect("inherited" in issue).toBe(false);
1094
+ // own symbol-keyed fields are not copied either; nothing in zod produces one and the walk stays a string-key walk
1095
+ const sym = Symbol("meta");
1096
+ const symbolic = z.string().check((ctx) => {
1097
+ ctx.issues.push({ code: "custom", message: "sym", input: ctx.value, [sym]: 1 } as never);
1098
+ });
1099
+ expect(Object.getOwnPropertySymbols(symbolic.safeParse("x").error!.issues[0])).toEqual([]);
1100
+ // an own __proto__ key, as JSON.parse produces, neither swaps the prototype nor survives
1101
+ const parsed = z.string().check((ctx) => {
1102
+ ctx.issues.push(JSON.parse('{"code":"custom","message":"json","__proto__":{"polluted":true}}'));
1103
+ });
1104
+ const fromJson = parsed.safeParse("x").error!.issues[0];
1105
+ expect(Object.getPrototypeOf(fromJson)).toBe(Object.prototype);
1106
+ expect(Object.prototype.hasOwnProperty.call(fromJson, "__proto__")).toBe(false);
1107
+ expect("polluted" in fromJson).toBe(false);
1108
+ });
@@ -80,6 +80,8 @@ test("first party switch", () => {
80
80
  break;
81
81
  case "file":
82
82
  break;
83
+ case "properties":
84
+ break;
83
85
  case "lazy":
84
86
  break;
85
87
  case "function":
@@ -170,6 +172,8 @@ test("$ZodSchemaTypes", () => {
170
172
  break;
171
173
  case "lazy":
172
174
  break;
175
+ case "properties":
176
+ break;
173
177
  case "function":
174
178
  break;
175
179