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
@@ -0,0 +1,81 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import * as z from "zod";
3
+
4
+ describe("z.iban", () => {
5
+ const valid: [string, string][] = [
6
+ ["Norway (15, minimum)", "NO9386011117947"],
7
+ ["Belgium (16)", "BE68539007547034"],
8
+ ["Netherlands (18)", "NL91ABNA0417164300"],
9
+ ["Germany (22)", "DE89370400440532013000"],
10
+ ["Great Britain (22)", "GB29NWBK60161331926819"],
11
+ ["Turkey (26)", "TR610000100000000000000001"],
12
+ ["France (27)", "FR1420041010050500013M02606"],
13
+ ["Cyprus (28)", "CY17002001280000001200527600"],
14
+ ["Malta (31)", "MT84MALT011000012345MTLCAST001S"],
15
+ ["Maximum length (34)", "AA73AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
16
+ ["Maximum length mixed (34)", "ZZ560123456789ABCDEFGHIJKLMNOPQRST"],
17
+ ];
18
+
19
+ test.each(valid)("accepts %s", (_label, n) => {
20
+ expect(z.iban().parse(n)).toBe(n);
21
+ });
22
+
23
+ test("rejects lowercase characters", () => {
24
+ expect(() => z.iban().parse("no9386011117947")).toThrow();
25
+ expect(() => z.iban().parse("de89370400440532013000")).toThrow();
26
+ expect(() => z.iban().parse("FR1420041010050500013m02606")).toThrow();
27
+ });
28
+
29
+ test("rejects spaces and hyphens", () => {
30
+ expect(() => z.iban().parse("NO93 8601 1117 947")).toThrow();
31
+ expect(() => z.iban().parse(" DE89370400440532013000")).toThrow();
32
+ expect(() => z.iban().parse("DE89370400440532013000 ")).toThrow();
33
+ expect(() => z.iban().parse("NO93-8601-1117-947")).toThrow();
34
+ expect(() => z.iban().parse("DE89-3704-0044-0532-0130-00")).toThrow();
35
+ });
36
+
37
+ test("rejects special characters", () => {
38
+ expect(() => z.iban().parse("NO938601111794!")).toThrow();
39
+ expect(() => z.iban().parse("DE8937040044053201300@")).toThrow();
40
+ expect(() => z.iban().parse("")).toThrow();
41
+ });
42
+
43
+ test("rejects inputs outside length bounds (15-34)", () => {
44
+ expect(() => z.iban().parse("NO938601111794")).toThrow();
45
+ expect(() => z.iban().parse("AA73AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")).toThrow();
46
+ });
47
+
48
+ test("rejects invalid check digits (checksum failure)", () => {
49
+ expect(() => z.iban().parse("NO9386011117948")).toThrow();
50
+ expect(() => z.iban().parse("DE89370400440532013001")).toThrow();
51
+ expect(() => z.iban().parse("FR1420041010050500013M02607")).toThrow();
52
+ });
53
+
54
+ test("rejects check digits outside 02-98", () => {
55
+ // mod 97 alone accepts all three; `98 - remainder` can only ever emit 02-98
56
+ expect(() => z.iban().parse("DE00000000000000000066")).toThrow();
57
+ expect(() => z.iban().parse("DE01000000000000000048")).toThrow();
58
+ expect(() => z.iban().parse("DE99000000000000000030")).toThrow();
59
+ });
60
+
61
+ test("carries the shape regex as its JSON Schema pattern", () => {
62
+ expect(z.toJSONSchema(z.iban())).toMatchObject({
63
+ format: "iban",
64
+ pattern: z.regexes.iban.source,
65
+ });
66
+ });
67
+
68
+ test("round-trips through JSON Schema with the checksum intact", () => {
69
+ const schema = z.fromJSONSchema(z.toJSONSchema(z.iban()));
70
+ expect(schema.safeParse("DE89370400440532013000").success).toBe(true);
71
+ expect(schema.safeParse("DE89370400440532013001").success).toBe(false);
72
+ });
73
+
74
+ test("error message references IBAN", () => {
75
+ const result = z.iban().safeParse("not-an-iban");
76
+ expect(result.success).toBe(false);
77
+ if (!result.success) {
78
+ expect(result.error.issues[0]?.message).toMatch(/IBAN/i);
79
+ }
80
+ });
81
+ });
@@ -152,10 +152,16 @@ test("a live member keeps the descriptor a prototype member had", () => {
152
152
  const proto = Object.getPrototypeOf(z.string());
153
153
  expect(Object.getOwnPropertyDescriptor(proto, "description")?.enumerable).toBe(false);
154
154
  expect(Object.getOwnPropertyDescriptor(proto, "_def")?.enumerable).toBe(false);
155
+ // a derived member installs like a literal's accessor: not enumerable, still configurable
156
+ expect(Object.getOwnPropertyDescriptor(proto, "minLength")).toMatchObject({ enumerable: false, configurable: true });
155
157
 
158
+ // the derived metadata members live on the prototype and shadow as own data on first read
159
+ const schema = z.string();
156
160
  const keys: string[] = [];
157
- for (const k in z.string()) keys.push(k);
158
- expect(keys).toEqual(["def", "type", "format", "minLength", "maxLength"]);
161
+ for (const k in schema) keys.push(k);
162
+ expect(keys).toEqual(["def", "type"]);
163
+ expect(schema.minLength).toBe(null);
164
+ expect(Object.keys(schema)).toContain("minLength");
159
165
  });
160
166
 
161
167
  test("a live member is not cached per instance", () => {
@@ -221,6 +227,16 @@ test("a hand-written getter member accepts assignment", () => {
221
227
  const mini: any = zm.string();
222
228
  mini.with = () => "WITH";
223
229
  expect(mini.with()).toBe("WITH");
230
+
231
+ // a derived metadata member accepts assignment before its first read, as it did as an own property
232
+ const derived = z.number().min(2);
233
+ derived.minValue = 99;
234
+ expect(derived.minValue).toBe(99);
235
+ expect(Object.keys(derived)).toContain("minValue");
236
+
237
+ // and, like every prototype member, recomputes after deletion rather than staying absent
238
+ delete (derived as any).minValue;
239
+ expect(derived.minValue).toBe(2);
224
240
  });
225
241
 
226
242
  test("shape is lazy and stays out of Object.keys", () => {
@@ -62,7 +62,7 @@ test("instanceof respects customError", () => {
62
62
  test("z.properties", () => {
63
63
  const httpsUrl = z.instanceof(URL).check(
64
64
  ...z.properties({
65
- protocol: z.literal("https:" as string),
65
+ protocol: z.literal("https:"),
66
66
  hostname: z.string().regex(z.regexes.domain),
67
67
  })
68
68
  );
@@ -70,9 +70,9 @@ test("z.properties", () => {
70
70
  expectTypeOf<URL>().toEqualTypeOf<z.infer<typeof httpsUrl>>();
71
71
  expect(httpsUrl.safeParse(new URL("https://example.com")).success).toBe(true);
72
72
 
73
- // Checks abort on the first failure, exactly as the equivalent chain of z.property() calls does.
73
+ // One check iterates the whole shape, so every failing property reports — unlike the equivalent chain of z.property() calls, which aborts on the first.
74
74
  const both = httpsUrl.safeParse(new URL("http://localhost"));
75
- expect(both.error!.issues.map((i) => i.path)).toEqual([["protocol"]]);
75
+ expect(both.error!.issues.map((i) => i.path)).toEqual([["protocol"], ["hostname"]]);
76
76
 
77
77
  // A failing base schema yields its own issue and no property issues.
78
78
  for (const input of ["not a url", null]) {
@@ -85,12 +85,12 @@ test("z.properties", () => {
85
85
  .object({ a: z.string(), b: z.string() })
86
86
  .check(...z.properties({ a: z.literal("x"), b: z.literal("y") }));
87
87
  expect(obj.safeParse({ a: "x", b: "y" }).success).toBe(true);
88
- expect(obj.safeParse({ a: "!", b: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"]]);
88
+ expect(obj.safeParse({ a: "!", b: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"], ["b"]]);
89
89
  for (const input of [null, undefined, 5]) {
90
90
  expect(obj.safeParse(input).error!.issues.map((i) => [i.code, i.path])).toEqual([["invalid_type", []]]);
91
91
  }
92
92
 
93
- // Known looseness versus the longhand, pinned so it stays deliberate: every element is typed over the whole shape, and `$ZodCheckInternals.check()` is a method, so TypeScript compares it bivariantly and accepts a check type that is a subtype of the target. Naming a key the target lacks therefore compiles here and fails at parse time, where the equivalent chain of `z.property()` calls rejects it outright. Typing each element over only its own key fixes that and breaks every valid call, since a union argument must satisfy the target on its own.
93
+ // Known looseness versus the longhand, pinned so it stays deliberate: the schema-as-check is typed over the whole shape, and `$ZodCheckInternals.check()` is a method, so TypeScript compares it bivariantly and accepts a check type that is a subtype of the target. Naming a key the target lacks therefore compiles here and fails at parse time, where the equivalent chain of `z.property()` calls rejects it outright.
94
94
  z.object({ a: z.string() }).check(...z.properties({ a: z.literal("x"), b: z.literal("y") }));
95
95
  expect(
96
96
  z
@@ -100,8 +100,28 @@ test("z.properties", () => {
100
100
  .error!.issues.map((i) => [i.code, i.path])
101
101
  ).toEqual([["invalid_value", ["b"]]]);
102
102
 
103
- // Plain property checks carry no `when`, so the schema stays on the compiled fast path.
103
+ // literal and enum inputs widen to their primitive, so a `string`/`boolean` property accepts them without a cast, while a genuine type mismatch or an unknown key still fails to compile
104
+ z.instanceof(URL).check(z.property("protocol", z.literal("https:")));
105
+ z.instanceof(Request).check(...z.properties({ method: z.enum(["POST", "PUT"]), bodyUsed: z.literal(false) }));
106
+ // @ts-expect-error length is a number
107
+ z.string().check(z.property("length", z.string()));
108
+ // @ts-expect-error no such key
109
+ z.instanceof(URL).check(z.property("nope", z.string()));
110
+
111
+ // the check feeds the property value into its schema, so it is typed over the input side rather than the output
112
+ const stringToLength = z.property(
113
+ "a",
114
+ z.string().transform((s) => s.length)
115
+ );
116
+ z.object({ a: z.string() }).check(stringToLength);
117
+ // @ts-expect-error a is a number, the schema takes a string
118
+ z.object({ a: z.number() }).check(stringToLength);
119
+
120
+ // The properties check carries no `when`, so the schema stays on the compiled fast path.
104
121
  const compiled = z.compile(httpsUrl);
105
122
  expect(compiled.safeParse(new URL("https://example.com")).success).toBe(true);
106
- expect(compiled.safeParse(new URL("http://localhost")).error!.issues.map((i) => i.path)).toEqual([["protocol"]]);
123
+ expect(compiled.safeParse(new URL("http://localhost")).error!.issues.map((i) => i.path)).toEqual([
124
+ ["protocol"],
125
+ ["hostname"],
126
+ ]);
107
127
  });
@@ -5,6 +5,9 @@ afterEach(() => {
5
5
  z.config({ customError: undefined });
6
6
  });
7
7
 
8
+ /** Error maps run on the first read of `error`, so a test that only observes them reads it. */
9
+ const fail = (r: { error?: unknown }) => r.error;
10
+
8
11
  /** Collects the owning schema's registered metadata for every issue an error map sees. */
9
12
  function captureMeta(): { seen: (z.core.GlobalMeta | undefined)[] } {
10
13
  const seen: (z.core.GlobalMeta | undefined)[] = [];
@@ -20,11 +23,11 @@ function captureMeta(): { seen: (z.core.GlobalMeta | undefined)[] } {
20
23
  test("check-originated issues expose the owning schema", () => {
21
24
  const { seen } = captureMeta();
22
25
 
23
- z.object({ name: z.string().min(5).meta({ title: "Name" }) }).safeParse({ name: "ab" });
24
- z.string().max(2).meta({ title: "Max" }).safeParse("abc");
25
- z.email().meta({ title: "Email" }).safeParse("nope");
26
- z.number().multipleOf(3).meta({ title: "Multiple" }).safeParse(4);
27
- z.array(z.string()).min(2).meta({ title: "Array" }).safeParse([]);
26
+ fail(z.object({ name: z.string().min(5).meta({ title: "Name" }) }).safeParse({ name: "ab" }));
27
+ fail(z.string().max(2).meta({ title: "Max" }).safeParse("abc"));
28
+ fail(z.email().meta({ title: "Email" }).safeParse("nope"));
29
+ fail(z.number().multipleOf(3).meta({ title: "Multiple" }).safeParse(4));
30
+ fail(z.array(z.string()).min(2).meta({ title: "Array" }).safeParse([]));
28
31
 
29
32
  expect(seen).toEqual([
30
33
  { title: "Name" },
@@ -38,8 +41,8 @@ test("check-originated issues expose the owning schema", () => {
38
41
  test("schema-originated issues expose the owning schema", () => {
39
42
  const { seen } = captureMeta();
40
43
 
41
- z.object({ name: z.string().min(5).meta({ title: "Name" }) }).safeParse({ name: 123 });
42
- z.email().meta({ title: "Email" }).safeParse(123);
44
+ fail(z.object({ name: z.string().min(5).meta({ title: "Name" }) }).safeParse({ name: 123 }));
45
+ fail(z.email().meta({ title: "Email" }).safeParse(123));
43
46
 
44
47
  expect(seen).toEqual([{ title: "Name" }, { title: "Email" }]);
45
48
  });
@@ -47,14 +50,20 @@ test("schema-originated issues expose the owning schema", () => {
47
50
  test("refinements attribute to the refined schema, not the internal check schema", () => {
48
51
  const { seen } = captureMeta();
49
52
 
50
- z.string()
51
- .refine(() => false)
52
- .meta({ title: "Refine" })
53
- .safeParse("abc");
54
- z.string()
55
- .superRefine((_, ctx) => ctx.addIssue({ code: "custom", message: "" }))
56
- .meta({ title: "SuperRefine" })
57
- .safeParse("abc");
53
+ fail(
54
+ z
55
+ .string()
56
+ .refine(() => false)
57
+ .meta({ title: "Refine" })
58
+ .safeParse("abc")
59
+ );
60
+ fail(
61
+ z
62
+ .string()
63
+ .superRefine((_, ctx) => ctx.addIssue({ code: "custom", message: "" }))
64
+ .meta({ title: "SuperRefine" })
65
+ .safeParse("abc")
66
+ );
58
67
 
59
68
  expect(seen).toEqual([{ title: "Refine" }, { title: "SuperRefine" }]);
60
69
  });
@@ -62,17 +71,26 @@ test("refinements attribute to the refined schema, not the internal check schema
62
71
  test("the innermost schema owns the issue", () => {
63
72
  const { seen } = captureMeta();
64
73
 
65
- z.array(z.string().min(5).meta({ title: "Element" }))
66
- .meta({ title: "Array" })
67
- .safeParse(["ab"]);
68
- z.string()
69
- .pipe(z.string().min(5).meta({ title: "Target" }))
70
- .meta({ title: "Pipe" })
71
- .safeParse("ab");
72
- z.looseObject({})
73
- .check(z.property("a", z.string().meta({ title: "Property" })))
74
- .meta({ title: "Object" })
75
- .safeParse({ a: 1 });
74
+ fail(
75
+ z
76
+ .array(z.string().min(5).meta({ title: "Element" }))
77
+ .meta({ title: "Array" })
78
+ .safeParse(["ab"])
79
+ );
80
+ fail(
81
+ z
82
+ .string()
83
+ .pipe(z.string().min(5).meta({ title: "Target" }))
84
+ .meta({ title: "Pipe" })
85
+ .safeParse("ab")
86
+ );
87
+ fail(
88
+ z
89
+ .looseObject({})
90
+ .check(z.property("a", z.string().meta({ title: "Property" })))
91
+ .meta({ title: "Object" })
92
+ .safeParse({ a: 1 })
93
+ );
76
94
 
77
95
  expect(seen).toEqual([{ title: "Element" }, { title: "Target" }, { title: "Property" }]);
78
96
  });
@@ -81,12 +99,15 @@ test("issues carrying no usable `inst` still get the owning schema", () => {
81
99
  const { seen } = captureMeta();
82
100
 
83
101
  // A hand-pushed issue may carry no inst at all, and ctx.addIssue(string) puts the check's def — not a Zod object — on inst.
84
- z.string()
85
- .check((payload) => {
86
- payload.issues.push({ code: "custom", input: payload.value });
87
- })
88
- .meta({ title: "Bare" })
89
- .safeParse("abc");
102
+ fail(
103
+ z
104
+ .string()
105
+ .check((payload) => {
106
+ payload.issues.push({ code: "custom", input: payload.value });
107
+ })
108
+ .meta({ title: "Bare" })
109
+ .safeParse("abc")
110
+ );
90
111
  const shorthand = z
91
112
  .string()
92
113
  .superRefine((_, ctx) => ctx.addIssue("bad stuff"))
@@ -100,11 +121,13 @@ test("issues carrying no usable `inst` still get the owning schema", () => {
100
121
  test("async checks expose the owning schema", async () => {
101
122
  const { seen } = captureMeta();
102
123
 
103
- await z
104
- .string()
105
- .refine(async () => false)
106
- .meta({ title: "Async" })
107
- .safeParseAsync("abc");
124
+ fail(
125
+ await z
126
+ .string()
127
+ .refine(async () => false)
128
+ .meta({ title: "Async" })
129
+ .safeParseAsync("abc")
130
+ );
108
131
 
109
132
  expect(seen).toEqual([{ title: "Async" }]);
110
133
  });
@@ -120,7 +143,7 @@ test("the owning schema is the clone `.meta()` registered, not the pre-metadata
120
143
  },
121
144
  });
122
145
 
123
- labeled.safeParse("ab");
146
+ fail(labeled.safeParse("ab"));
124
147
 
125
148
  expect(seen).toEqual([true]);
126
149
  });
@@ -134,12 +157,12 @@ test("`inst` keeps its meaning and `schema` stays off the finalized issue", () =
134
157
  },
135
158
  });
136
159
 
137
- const tooSmall = z.string().min(5).safeParse("ab");
138
- const wrongType = z.string().safeParse(123);
160
+ const tooSmall = z.string().min(5).safeParse("ab").error!;
161
+ const wrongType = z.string().safeParse(123).error!;
139
162
 
140
163
  expect(insts).toEqual(["$ZodCheckMinLength", "ZodString"]);
141
- expect(Object.keys(tooSmall.error!.issues[0])).not.toContain("schema");
142
- expect(Object.keys(wrongType.error!.issues[0])).not.toContain("schema");
164
+ expect(Object.keys(tooSmall.issues[0])).not.toContain("schema");
165
+ expect(Object.keys(wrongType.issues[0])).not.toContain("schema");
143
166
  });
144
167
 
145
168
  test("every error map kind receives the owning schema", () => {
@@ -149,28 +172,37 @@ test("every error map kind receives the owning schema", () => {
149
172
  return undefined;
150
173
  };
151
174
 
152
- z.string()
153
- .min(5, { error: capture("check") })
154
- .meta({ title: "Check" })
155
- .safeParse("ab");
175
+ fail(
176
+ z
177
+ .string()
178
+ .min(5, { error: capture("check") })
179
+ .meta({ title: "Check" })
180
+ .safeParse("ab")
181
+ );
156
182
 
157
183
  // A schema-bound map is only consulted for issues the schema itself raised, so this one needs a type error.
158
- z.string({ error: capture("schema") })
159
- .meta({ title: "Schema" })
160
- .safeParse(123);
161
-
162
- z.string()
163
- .min(5)
164
- .meta({ title: "Parse" })
165
- .safeParse("ab", { error: capture("parse") });
184
+ fail(
185
+ z
186
+ .string({ error: capture("schema") })
187
+ .meta({ title: "Schema" })
188
+ .safeParse(123)
189
+ );
190
+
191
+ fail(
192
+ z
193
+ .string()
194
+ .min(5)
195
+ .meta({ title: "Parse" })
196
+ .safeParse("ab", { error: capture("parse") })
197
+ );
166
198
 
167
199
  z.config({ customError: capture("customError") });
168
- z.string().min(5).meta({ title: "Custom" }).safeParse("ab");
200
+ fail(z.string().min(5).meta({ title: "Custom" }).safeParse("ab"));
169
201
 
170
202
  // localeError carries the default English locale, so it is restored rather than cleared.
171
203
  const localeError = z.config().localeError;
172
204
  z.config({ customError: undefined, localeError: capture("localeError") });
173
- z.string().min(5).meta({ title: "Locale" }).safeParse("ab");
205
+ fail(z.string().min(5).meta({ title: "Locale" }).safeParse("ab"));
174
206
  z.config({ localeError });
175
207
 
176
208
  expect(seen).toEqual({
@@ -838,6 +838,62 @@ describe("__proto__ as a declared shape key", () => {
838
838
  expect(() => (schema[method] as any)(mask).shape).toThrow('Unrecognized key: "__proto__"');
839
839
  });
840
840
 
841
+ // the mask is checked against the source's declared keys, which reads without resolving them, so it throws where the mistake is
842
+ test.each(["pick", "omit", "partial", "required"] as const)("%s rejects an undeclared key at the call", (method) => {
843
+ const schema = z.object({ value: z.string() });
844
+
845
+ expect(() => (schema[method] as any)({ nope: true })).toThrow('Unrecognized key: "nope"');
846
+ });
847
+
848
+ // a derived shape is built key by key, and a plain assignment runs whatever setter answers to the key — `__proto__` always has one, and a polluted prototype can supply one for any name
849
+ test("an inherited setter cannot swallow a derived key", () => {
850
+ let swallowed: unknown;
851
+ Object.defineProperty(Object.prototype, "field", {
852
+ configurable: true,
853
+ set(v) {
854
+ swallowed = v;
855
+ },
856
+ get() {
857
+ return undefined;
858
+ },
859
+ });
860
+
861
+ try {
862
+ const derived = z.object({ field: z.string() }).extend({ extra: z.number() });
863
+
864
+ expect(Object.keys(derived.shape)).toEqual(["field", "extra"]);
865
+ expect(swallowed).toBeUndefined();
866
+ // the key is still validated; what a parse writes into its result is the ambient prototype's business, as it is without a builder
867
+ expect(derived.safeParse({ field: 123, extra: 1 }).success).toBe(false);
868
+ expect(derived.safeParse({ field: "a", extra: 1 }).success).toBe(true);
869
+ } finally {
870
+ delete (Object.prototype as any).field;
871
+ }
872
+ });
873
+
874
+ // a shape resolves by object spread, so a non-enumerable entry is no part of it and no builder may promote one into a derived shape
875
+ test("a non-enumerable shape entry stays out of every derived shape", () => {
876
+ const sym = Symbol("kept");
877
+ const hide = (target: Record<string, any>) =>
878
+ Object.defineProperty(target, "hidden", { value: z.string(), enumerable: false, configurable: true });
879
+ // each case needs a source whose shape is still unresolved, since resolving drops the entry on its own
880
+ const source = () => z.object(hide({ b: z.number(), [sym]: z.boolean() }) as any);
881
+
882
+ expect(Reflect.ownKeys(source().shape)).toEqual(["b", sym]);
883
+ expect(Reflect.ownKeys(source().extend({ c: z.boolean() }).shape)).toEqual(["b", "c", sym]);
884
+ expect(Reflect.ownKeys(source().partial().shape)).toEqual(["b", sym]);
885
+ expect(Reflect.ownKeys(source().merge(z.object({ c: z.boolean() })).shape)).toEqual(["b", "c", sym]);
886
+ expect(Object.keys(z.object({ b: z.number() }).extend(hide({}) as any).shape)).toEqual(["b"]);
887
+ expect(() => source().pick({ hidden: true } as any)).toThrow('Unrecognized key: "hidden"');
888
+
889
+ // promoting it would make a key the source ignores required
890
+ expect(
891
+ source()
892
+ .extend({ c: z.boolean() })
893
+ .safeParse({ b: 1, [sym]: true, c: true }).success
894
+ ).toBe(true);
895
+ });
896
+
841
897
  test("shape helpers preserve a declared key", () => {
842
898
  const shape = () =>
843
899
  Object.fromEntries([
@@ -222,7 +222,8 @@ test("ladder: the rung values themselves", () => {
222
222
 
223
223
  test("exactOptional overrides the values and pattern optional installs", () => {
224
224
  expect(z.exactOptional(z.enum(["a", "b"]))._zod.values).toEqual(new Set(["a", "b"]));
225
- expect(z.exactOptional(z.string().regex(/^abc$/))._zod.pattern).toEqual(/^abc$/);
225
+ // a format carries its pattern on its def; a chained `.regex()` no longer folds into the inner `_zod.pattern`, the template literal derives that itself
226
+ expect(z.exactOptional(z.stringFormat("abc", /^abc$/))._zod.pattern).toEqual(/^abc$/);
226
227
  expect(z.templateLiteral(["a", z.exactOptional(z.literal("b"))]).safeParse("a").success).toEqual(false);
227
228
  });
228
229
 
@@ -230,5 +231,5 @@ test("exactOptional overrides the values and pattern optional installs", () => {
230
231
  test("exactOptional's override holds for later instances", () => {
231
232
  z.exactOptional(z.enum(["a", "b"]));
232
233
  expect(z.exactOptional(z.enum(["c", "d"]))._zod.values).toEqual(new Set(["c", "d"]));
233
- expect(z.exactOptional(z.string().regex(/^xyz$/))._zod.pattern).toEqual(/^xyz$/);
234
+ expect(z.exactOptional(z.stringFormat("xyz", /^xyz$/))._zod.pattern).toEqual(/^xyz$/);
234
235
  });