zod 4.5.3 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/compile.cjs +19 -0
  2. package/index.cjs +19 -0
  3. package/locales/index.cjs +19 -0
  4. package/mini/index.cjs +19 -0
  5. package/package.json +8 -7
  6. package/src/v3/helpers/errorUtil.ts +1 -1
  7. package/src/v4/classic/checks.ts +0 -1
  8. package/src/v4/classic/external.ts +2 -0
  9. package/src/v4/classic/from-json-schema.ts +248 -25
  10. package/src/v4/classic/schemas.ts +255 -164
  11. package/src/v4/classic/tests/assignability.test.ts +59 -9
  12. package/src/v4/classic/tests/bigint.test.ts +6 -0
  13. package/src/v4/classic/tests/continuability.test.ts +2 -2
  14. package/src/v4/classic/tests/cyclic-data.test.ts +316 -0
  15. package/src/v4/classic/tests/default.test.ts +46 -0
  16. package/src/v4/classic/tests/enum.test.ts +9 -0
  17. package/src/v4/classic/tests/error.test.ts +130 -1
  18. package/src/v4/classic/tests/firstparty.test.ts +4 -0
  19. package/src/v4/classic/tests/from-json-schema.test.ts +157 -0
  20. package/src/v4/classic/tests/iban.test.ts +81 -0
  21. package/src/v4/classic/tests/instance-footprint.test.ts +18 -2
  22. package/src/v4/classic/tests/instanceof.test.ts +27 -7
  23. package/src/v4/classic/tests/issue-schema.test.ts +88 -56
  24. package/src/v4/classic/tests/object.test.ts +56 -0
  25. package/src/v4/classic/tests/optin-ladder.test.ts +3 -2
  26. package/src/v4/classic/tests/properties.test.ts +225 -0
  27. package/src/v4/classic/tests/recursive-types.test.ts +1 -1
  28. package/src/v4/classic/tests/standard-schema.test.ts +19 -0
  29. package/src/v4/classic/tests/string.test.ts +25 -0
  30. package/src/v4/classic/tests/template-literal.test.ts +33 -1
  31. package/src/v4/classic/tests/to-json-schema.test.ts +147 -3
  32. package/src/v4/classic/tests/validate.test.ts +569 -0
  33. package/src/v4/core/api.ts +30 -6
  34. package/src/v4/core/checks.ts +1 -101
  35. package/src/v4/core/compile.ts +193 -81
  36. package/src/v4/core/doc.ts +6 -2
  37. package/src/v4/core/errors.ts +3 -6
  38. package/src/v4/core/json-schema-generator.ts +2 -2
  39. package/src/v4/core/json-schema-processors.ts +226 -68
  40. package/src/v4/core/memoizer.ts +162 -36
  41. package/src/v4/core/parse.ts +37 -16
  42. package/src/v4/core/regexes.ts +10 -4
  43. package/src/v4/core/schemas.ts +314 -83
  44. package/src/v4/core/tests/cached.test.ts +54 -0
  45. package/src/v4/core/tests/compile-generative.test.ts +445 -0
  46. package/src/v4/core/tests/compile.test.ts +222 -2
  47. package/src/v4/core/tests/email-regex.test.ts +140 -0
  48. package/src/v4/core/tests/polyfill-collision.test.ts +46 -0
  49. package/src/v4/core/to-json-schema.ts +10 -8
  50. package/src/v4/core/util.ts +222 -147
  51. package/src/v4/core/versions.ts +2 -2
  52. package/src/v4/core/visit.ts +11 -0
  53. package/src/v4/locales/ar.ts +1 -0
  54. package/src/v4/locales/az.ts +1 -0
  55. package/src/v4/locales/be.ts +1 -0
  56. package/src/v4/locales/bg.ts +1 -0
  57. package/src/v4/locales/bn.ts +1 -0
  58. package/src/v4/locales/ca.ts +1 -0
  59. package/src/v4/locales/ckb.ts +1 -0
  60. package/src/v4/locales/cs.ts +1 -0
  61. package/src/v4/locales/da.ts +1 -0
  62. package/src/v4/locales/de.ts +1 -0
  63. package/src/v4/locales/el.ts +1 -0
  64. package/src/v4/locales/en.ts +1 -0
  65. package/src/v4/locales/eo.ts +1 -0
  66. package/src/v4/locales/es.ts +1 -0
  67. package/src/v4/locales/fa.ts +1 -0
  68. package/src/v4/locales/fi.ts +1 -0
  69. package/src/v4/locales/fr-CA.ts +1 -0
  70. package/src/v4/locales/fr.ts +1 -0
  71. package/src/v4/locales/gu.ts +1 -0
  72. package/src/v4/locales/he.ts +1 -0
  73. package/src/v4/locales/hi.ts +1 -0
  74. package/src/v4/locales/hr.ts +1 -0
  75. package/src/v4/locales/hu.ts +1 -0
  76. package/src/v4/locales/hy.ts +1 -0
  77. package/src/v4/locales/id.ts +1 -0
  78. package/src/v4/locales/is.ts +1 -0
  79. package/src/v4/locales/it.ts +1 -0
  80. package/src/v4/locales/ja.ts +1 -0
  81. package/src/v4/locales/ka.ts +1 -0
  82. package/src/v4/locales/km.ts +1 -0
  83. package/src/v4/locales/kn.ts +1 -0
  84. package/src/v4/locales/ko.ts +1 -0
  85. package/src/v4/locales/lt.ts +1 -0
  86. package/src/v4/locales/mk.ts +1 -0
  87. package/src/v4/locales/ms.ts +1 -0
  88. package/src/v4/locales/ne.ts +1 -0
  89. package/src/v4/locales/nl.ts +1 -0
  90. package/src/v4/locales/nn.ts +1 -0
  91. package/src/v4/locales/no.ts +1 -0
  92. package/src/v4/locales/ota.ts +1 -0
  93. package/src/v4/locales/pl.ts +1 -0
  94. package/src/v4/locales/ps.ts +1 -0
  95. package/src/v4/locales/pt-BR.ts +1 -0
  96. package/src/v4/locales/pt.ts +1 -0
  97. package/src/v4/locales/ro.ts +1 -0
  98. package/src/v4/locales/ru.ts +1 -0
  99. package/src/v4/locales/sk.ts +1 -0
  100. package/src/v4/locales/sl.ts +1 -0
  101. package/src/v4/locales/sv.ts +1 -0
  102. package/src/v4/locales/ta.ts +1 -0
  103. package/src/v4/locales/th.ts +1 -0
  104. package/src/v4/locales/tk.ts +1 -0
  105. package/src/v4/locales/tr.ts +1 -0
  106. package/src/v4/locales/uk.ts +1 -0
  107. package/src/v4/locales/ur.ts +1 -0
  108. package/src/v4/locales/uz.ts +1 -0
  109. package/src/v4/locales/vi.ts +1 -0
  110. package/src/v4/locales/yo.ts +1 -0
  111. package/src/v4/locales/zh-CN.ts +1 -0
  112. package/src/v4/locales/zh-TW.ts +1 -0
  113. package/src/v4/mini/checks.ts +0 -1
  114. package/src/v4/mini/external.ts +2 -0
  115. package/src/v4/mini/schemas.ts +38 -2
  116. package/src/v4/mini/tests/checks.test.ts +1 -1
  117. package/src/v4/mini/tests/computed.test.ts +11 -17
  118. package/src/v4/mini/tests/index.test.ts +8 -0
  119. package/src/v4/mini/tests/string.test.ts +16 -0
  120. package/v3/ZodError.cjs +19 -0
  121. package/v3/errors.cjs +19 -0
  122. package/v3/external.cjs +19 -0
  123. package/v3/helpers/enumUtil.cjs +19 -0
  124. package/v3/helpers/errorUtil.cjs +20 -1
  125. package/v3/helpers/errorUtil.js +1 -1
  126. package/v3/helpers/parseUtil.cjs +19 -0
  127. package/v3/helpers/partialUtil.cjs +19 -0
  128. package/v3/helpers/typeAliases.cjs +19 -0
  129. package/v3/helpers/util.cjs +19 -0
  130. package/v3/index.cjs +19 -0
  131. package/v3/standard-schema.cjs +19 -0
  132. package/v3/types.cjs +19 -0
  133. package/v4/classic/checks.cjs +20 -2
  134. package/v4/classic/checks.d.cts +1 -1
  135. package/v4/classic/checks.d.ts +1 -1
  136. package/v4/classic/checks.js +1 -1
  137. package/v4/classic/coerce.cjs +19 -0
  138. package/v4/classic/compat.cjs +19 -0
  139. package/v4/classic/deep-partial.cjs +19 -0
  140. package/v4/classic/errors.cjs +19 -0
  141. package/v4/classic/external.cjs +22 -1
  142. package/v4/classic/external.d.cts +1 -1
  143. package/v4/classic/external.d.ts +1 -1
  144. package/v4/classic/external.js +1 -1
  145. package/v4/classic/from-json-schema.cjs +264 -24
  146. package/v4/classic/from-json-schema.js +245 -24
  147. package/v4/classic/in-out.cjs +19 -0
  148. package/v4/classic/index.cjs +19 -0
  149. package/v4/classic/iso.cjs +19 -0
  150. package/v4/classic/parse.cjs +19 -0
  151. package/v4/classic/schemas.cjs +100 -31
  152. package/v4/classic/schemas.d.cts +17 -1
  153. package/v4/classic/schemas.d.ts +17 -1
  154. package/v4/classic/schemas.js +77 -29
  155. package/v4/core/api.cjs +37 -2
  156. package/v4/core/api.d.cts +6 -4
  157. package/v4/core/api.d.ts +6 -4
  158. package/v4/core/api.js +17 -2
  159. package/v4/core/checks.cjs +19 -96
  160. package/v4/core/checks.d.cts +1 -1
  161. package/v4/core/checks.d.ts +1 -1
  162. package/v4/core/checks.js +0 -96
  163. package/v4/core/compile.cjs +164 -65
  164. package/v4/core/compile.d.cts +18 -4
  165. package/v4/core/compile.d.ts +18 -4
  166. package/v4/core/compile.js +145 -66
  167. package/v4/core/core.cjs +19 -0
  168. package/v4/core/doc.cjs +26 -2
  169. package/v4/core/doc.js +7 -2
  170. package/v4/core/errors.cjs +21 -5
  171. package/v4/core/errors.js +2 -5
  172. package/v4/core/index.cjs +19 -0
  173. package/v4/core/json-schema-generator.cjs +20 -1
  174. package/v4/core/json-schema-generator.js +2 -2
  175. package/v4/core/json-schema-processors.cjs +215 -63
  176. package/v4/core/json-schema-processors.d.cts +17 -0
  177. package/v4/core/json-schema-processors.d.ts +17 -0
  178. package/v4/core/json-schema-processors.js +195 -64
  179. package/v4/core/json-schema.cjs +19 -0
  180. package/v4/core/memoizer.cjs +204 -36
  181. package/v4/core/memoizer.js +162 -36
  182. package/v4/core/parse.cjs +56 -16
  183. package/v4/core/parse.js +37 -16
  184. package/v4/core/regexes.cjs +29 -6
  185. package/v4/core/regexes.d.cts +2 -0
  186. package/v4/core/regexes.d.ts +2 -0
  187. package/v4/core/regexes.js +8 -4
  188. package/v4/core/registries.cjs +19 -0
  189. package/v4/core/schemas.cjs +285 -66
  190. package/v4/core/schemas.d.cts +34 -3
  191. package/v4/core/schemas.d.ts +34 -3
  192. package/v4/core/schemas.js +264 -65
  193. package/v4/core/standard-schema.cjs +19 -0
  194. package/v4/core/to-json-schema.cjs +29 -7
  195. package/v4/core/to-json-schema.d.cts +7 -3
  196. package/v4/core/to-json-schema.d.ts +7 -3
  197. package/v4/core/to-json-schema.js +8 -7
  198. package/v4/core/util.cjs +181 -143
  199. package/v4/core/util.d.cts +25 -4
  200. package/v4/core/util.d.ts +25 -4
  201. package/v4/core/util.js +160 -143
  202. package/v4/core/versions.cjs +21 -2
  203. package/v4/core/versions.d.cts +1 -1
  204. package/v4/core/versions.d.ts +1 -1
  205. package/v4/core/versions.js +2 -2
  206. package/v4/core/visit.cjs +31 -0
  207. package/v4/core/visit.js +12 -0
  208. package/v4/index.cjs +19 -0
  209. package/v4/locales/ar.cjs +1 -0
  210. package/v4/locales/ar.js +1 -0
  211. package/v4/locales/az.cjs +1 -0
  212. package/v4/locales/az.js +1 -0
  213. package/v4/locales/be.cjs +1 -0
  214. package/v4/locales/be.js +1 -0
  215. package/v4/locales/bg.cjs +1 -0
  216. package/v4/locales/bg.js +1 -0
  217. package/v4/locales/bn.cjs +1 -0
  218. package/v4/locales/bn.js +1 -0
  219. package/v4/locales/ca.cjs +1 -0
  220. package/v4/locales/ca.js +1 -0
  221. package/v4/locales/ckb.cjs +1 -0
  222. package/v4/locales/ckb.js +1 -0
  223. package/v4/locales/cs.cjs +1 -0
  224. package/v4/locales/cs.js +1 -0
  225. package/v4/locales/da.cjs +1 -0
  226. package/v4/locales/da.js +1 -0
  227. package/v4/locales/de.cjs +1 -0
  228. package/v4/locales/de.js +1 -0
  229. package/v4/locales/el.cjs +1 -0
  230. package/v4/locales/el.js +1 -0
  231. package/v4/locales/en.cjs +1 -0
  232. package/v4/locales/en.js +1 -0
  233. package/v4/locales/eo.cjs +1 -0
  234. package/v4/locales/eo.js +1 -0
  235. package/v4/locales/es.cjs +1 -0
  236. package/v4/locales/es.js +1 -0
  237. package/v4/locales/fa.cjs +1 -0
  238. package/v4/locales/fa.js +1 -0
  239. package/v4/locales/fi.cjs +1 -0
  240. package/v4/locales/fi.js +1 -0
  241. package/v4/locales/fr-CA.cjs +1 -0
  242. package/v4/locales/fr-CA.js +1 -0
  243. package/v4/locales/fr.cjs +1 -0
  244. package/v4/locales/fr.js +1 -0
  245. package/v4/locales/gu.cjs +1 -0
  246. package/v4/locales/gu.js +1 -0
  247. package/v4/locales/he.cjs +1 -0
  248. package/v4/locales/he.js +1 -0
  249. package/v4/locales/hi.cjs +1 -0
  250. package/v4/locales/hi.js +1 -0
  251. package/v4/locales/hr.cjs +1 -0
  252. package/v4/locales/hr.js +1 -0
  253. package/v4/locales/hu.cjs +1 -0
  254. package/v4/locales/hu.js +1 -0
  255. package/v4/locales/hy.cjs +1 -0
  256. package/v4/locales/hy.js +1 -0
  257. package/v4/locales/id.cjs +1 -0
  258. package/v4/locales/id.js +1 -0
  259. package/v4/locales/index.cjs +19 -0
  260. package/v4/locales/is.cjs +1 -0
  261. package/v4/locales/is.js +1 -0
  262. package/v4/locales/it.cjs +1 -0
  263. package/v4/locales/it.js +1 -0
  264. package/v4/locales/ja.cjs +1 -0
  265. package/v4/locales/ja.js +1 -0
  266. package/v4/locales/ka.cjs +1 -0
  267. package/v4/locales/ka.js +1 -0
  268. package/v4/locales/km.cjs +1 -0
  269. package/v4/locales/km.js +1 -0
  270. package/v4/locales/kn.cjs +1 -0
  271. package/v4/locales/kn.js +1 -0
  272. package/v4/locales/ko.cjs +1 -0
  273. package/v4/locales/ko.js +1 -0
  274. package/v4/locales/lt.cjs +1 -0
  275. package/v4/locales/lt.js +1 -0
  276. package/v4/locales/mk.cjs +1 -0
  277. package/v4/locales/mk.js +1 -0
  278. package/v4/locales/ms.cjs +1 -0
  279. package/v4/locales/ms.js +1 -0
  280. package/v4/locales/ne.cjs +1 -0
  281. package/v4/locales/ne.js +1 -0
  282. package/v4/locales/nl.cjs +1 -0
  283. package/v4/locales/nl.js +1 -0
  284. package/v4/locales/nn.cjs +1 -0
  285. package/v4/locales/nn.js +1 -0
  286. package/v4/locales/no.cjs +1 -0
  287. package/v4/locales/no.js +1 -0
  288. package/v4/locales/ota.cjs +1 -0
  289. package/v4/locales/ota.js +1 -0
  290. package/v4/locales/pl.cjs +1 -0
  291. package/v4/locales/pl.js +1 -0
  292. package/v4/locales/ps.cjs +1 -0
  293. package/v4/locales/ps.js +1 -0
  294. package/v4/locales/pt-BR.cjs +1 -0
  295. package/v4/locales/pt-BR.js +1 -0
  296. package/v4/locales/pt.cjs +1 -0
  297. package/v4/locales/pt.js +1 -0
  298. package/v4/locales/ro.cjs +1 -0
  299. package/v4/locales/ro.js +1 -0
  300. package/v4/locales/ru.cjs +1 -0
  301. package/v4/locales/ru.js +1 -0
  302. package/v4/locales/sk.cjs +1 -0
  303. package/v4/locales/sk.js +1 -0
  304. package/v4/locales/sl.cjs +1 -0
  305. package/v4/locales/sl.js +1 -0
  306. package/v4/locales/sv.cjs +1 -0
  307. package/v4/locales/sv.js +1 -0
  308. package/v4/locales/ta.cjs +1 -0
  309. package/v4/locales/ta.js +1 -0
  310. package/v4/locales/th.cjs +1 -0
  311. package/v4/locales/th.js +1 -0
  312. package/v4/locales/tk.cjs +1 -0
  313. package/v4/locales/tk.js +1 -0
  314. package/v4/locales/tr.cjs +1 -0
  315. package/v4/locales/tr.js +1 -0
  316. package/v4/locales/uk.cjs +1 -0
  317. package/v4/locales/uk.js +1 -0
  318. package/v4/locales/ur.cjs +1 -0
  319. package/v4/locales/ur.js +1 -0
  320. package/v4/locales/uz.cjs +1 -0
  321. package/v4/locales/uz.js +1 -0
  322. package/v4/locales/vi.cjs +1 -0
  323. package/v4/locales/vi.js +1 -0
  324. package/v4/locales/yo.cjs +1 -0
  325. package/v4/locales/yo.js +1 -0
  326. package/v4/locales/zh-CN.cjs +1 -0
  327. package/v4/locales/zh-CN.js +1 -0
  328. package/v4/locales/zh-TW.cjs +1 -0
  329. package/v4/locales/zh-TW.js +1 -0
  330. package/v4/mini/checks.cjs +20 -2
  331. package/v4/mini/checks.d.cts +1 -1
  332. package/v4/mini/checks.d.ts +1 -1
  333. package/v4/mini/checks.js +1 -1
  334. package/v4/mini/coerce.cjs +19 -0
  335. package/v4/mini/deep-partial.cjs +19 -0
  336. package/v4/mini/external.cjs +22 -1
  337. package/v4/mini/external.d.cts +1 -1
  338. package/v4/mini/external.d.ts +1 -1
  339. package/v4/mini/external.js +1 -1
  340. package/v4/mini/in-out.cjs +19 -0
  341. package/v4/mini/index.cjs +19 -0
  342. package/v4/mini/iso.cjs +19 -0
  343. package/v4/mini/parse.cjs +19 -0
  344. package/v4/mini/schemas.cjs +42 -4
  345. package/v4/mini/schemas.d.cts +8 -0
  346. package/v4/mini/schemas.d.ts +8 -0
  347. package/v4/mini/schemas.js +19 -2
  348. package/v4-mini/index.cjs +19 -0
@@ -0,0 +1,225 @@
1
+ import { expect, expectTypeOf, test } from "vitest";
2
+ import * as z from "zod/v4";
3
+
4
+ test("z.properties is a schema", () => {
5
+ const p = z.properties({ a: z.literal("x"), b: z.literal("y") });
6
+
7
+ expectTypeOf<z.infer<typeof p>>().toEqualTypeOf<{ a: "x"; b: "y" }>();
8
+
9
+ // asserts in place: the parse returns the input identity, extra keys pass
10
+ const ok = { a: "x", b: "y", extra: 1 };
11
+ expect(p.parse(ok)).toBe(ok);
12
+
13
+ // every failing key reports
14
+ expect(p.safeParse({ a: "!", b: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"], ["b"]]);
15
+
16
+ // as a schema it is the type gate and infers an object shape, so a primitive is a type error
17
+ for (const input of [null, undefined, 5, "abc"]) {
18
+ expect(p.safeParse(input).error!.issues.map((i) => [i.code, i.path])).toEqual([["invalid_type", []]]);
19
+ }
20
+
21
+ // as a check the base already typed the value, so the properties are asserted on whatever it produced — a string's length, as z.property() does
22
+ const long = z.string().check(...z.properties({ length: z.number().min(3) }));
23
+ expect(long.parse("abc")).toBe("abc");
24
+ expect(long.safeParse("ab").error!.issues.map((i) => i.path)).toEqual([["length"]]);
25
+ expect(
26
+ z
27
+ .compile(long)
28
+ .safeParse("ab")
29
+ .error!.issues.map((i) => i.path)
30
+ ).toEqual([["length"]]);
31
+
32
+ // an optional key may be absent
33
+ const opt = z.properties({ a: z.string().optional() });
34
+ expect(opt.safeParse({}).success).toBe(true);
35
+ expectTypeOf<z.infer<typeof opt>>().toEqualTypeOf<{ a?: string | undefined }>();
36
+ });
37
+
38
+ test("z.properties discards transformed output", () => {
39
+ // asserts and never writes back, exactly as z.property() behaves; a nested object schema rebuilds its output even without transforms, so identity cannot distinguish the two
40
+ const p = z.properties({ a: z.string().transform((s) => s.toUpperCase()) });
41
+ const input = { a: "hi" };
42
+ expect(p.parse(input)).toBe(input);
43
+ expect(input.a).toBe("hi");
44
+ });
45
+
46
+ test("z.properties narrows through intersection", () => {
47
+ const httpsUrl = z.instanceof(URL).and(z.properties({ protocol: z.literal("https:") }));
48
+ expectTypeOf<z.infer<typeof httpsUrl>>().toEqualTypeOf<URL & { protocol: "https:" }>();
49
+
50
+ const u = new URL("https://example.com");
51
+ expect(httpsUrl.parse(u)).toBe(u);
52
+ expect(httpsUrl.safeParse(new URL("http://example.com")).success).toBe(false);
53
+ });
54
+
55
+ test("z.instanceof().properties()", () => {
56
+ const httpsUrl = z.instanceof(URL).properties({ protocol: z.literal("https:") });
57
+ expectTypeOf<z.infer<typeof httpsUrl>>().toEqualTypeOf<URL & { protocol: "https:" }>();
58
+
59
+ const u = new URL("https://example.com");
60
+ expect(httpsUrl.parse(u)).toBe(u);
61
+ expect(httpsUrl.safeParse(new URL("http://example.com")).error!.issues.map((i) => i.path)).toEqual([["protocol"]]);
62
+
63
+ // chains, and each call narrows further
64
+ const chained = httpsUrl.properties({ port: z.literal("") });
65
+ expectTypeOf<z.infer<typeof chained>>().toEqualTypeOf<URL & { protocol: "https:" } & { port: "" }>();
66
+ expect(chained.safeParse(u).success).toBe(true);
67
+ expect(chained.safeParse(new URL("https://example.com:8443")).error!.issues.map((i) => i.path)).toEqual([["port"]]);
68
+ });
69
+
70
+ test("z.properties spreads into .check()", () => {
71
+ // the pre-4.6 array call sites: the schema yields itself from Symbol.iterator
72
+ const p = z.properties({ a: z.literal("x") });
73
+ const spread = [...p];
74
+ expect(spread).toEqual([p]);
75
+
76
+ const s = z.object({ a: z.string() }).check(...z.properties({ a: z.literal("x") }));
77
+ expect(s.safeParse({ a: "x" }).success).toBe(true);
78
+ expect(s.safeParse({ a: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"]]);
79
+ });
80
+
81
+ test("z.properties JSON Schema", () => {
82
+ const p = z.properties({ a: z.string(), b: z.number().optional() });
83
+ expect(z.toJSONSchema(p)).toEqual({
84
+ $schema: "https://json-schema.org/draft/2020-12/schema",
85
+ type: "object",
86
+ properties: { a: { type: "string" }, b: { type: "number" } },
87
+ required: ["a"],
88
+ });
89
+ });
90
+
91
+ test("z.properties compiles", () => {
92
+ const p = z.properties({ a: z.literal("x"), b: z.literal("y") });
93
+ const compiled = z.compile(p);
94
+ const ok = { a: "x", b: "y", extra: 1 };
95
+ expect(compiled.parse(ok)).toBe(ok);
96
+ expect(compiled.safeParse({ a: "!", b: "!" }).error!.issues.map((i) => i.path)).toEqual([["a"], ["b"]]);
97
+ expect(compiled.safeParse(null).error!.issues.map((i) => [i.code, i.path])).toEqual([["invalid_type", []]]);
98
+
99
+ // nested in a container
100
+ const outer = z.compile(z.object({ url: z.instanceof(URL).properties({ protocol: z.literal("https:") }) }));
101
+ expect(outer.safeParse({ url: new URL("http://example.com") }).error!.issues.map((i) => i.path)).toEqual([
102
+ ["url", "protocol"],
103
+ ]);
104
+ });
105
+
106
+ test("z.properties async", async () => {
107
+ const p = z.properties({ a: z.string().refine(async (s) => s.length > 1) });
108
+ const ok = { a: "hi" };
109
+ await expect(p.parseAsync(ok)).resolves.toBe(ok);
110
+ const bad = await p.safeParseAsync({ a: "!" });
111
+ expect(bad.error!.issues.map((i) => i.path)).toEqual([["a"]]);
112
+ });
113
+
114
+ test("z.properties infers the input side", () => {
115
+ // nothing is written back, so an output-typed inference would lie for every shape entry whose output differs from its input
116
+ const withDefault = z.properties({ a: z.string().default("x") });
117
+ expectTypeOf<z.infer<typeof withDefault>>().toEqualTypeOf<{ a?: string | undefined }>();
118
+ expect(withDefault.parse({})).toEqual({});
119
+
120
+ const withTransform = z.properties({ a: z.string().transform((s) => s.length) });
121
+ expectTypeOf<z.infer<typeof withTransform>>().toEqualTypeOf<{ a: string }>();
122
+ expect(withTransform.parse({ a: "hi" })).toEqual({ a: "hi" });
123
+
124
+ // a plain schema has the same input and output, so the documented narrowing is unchanged
125
+ const W = z.instanceof(URL).properties({ protocol: z.literal("https:") });
126
+ expectTypeOf<z.infer<typeof W>>().toEqualTypeOf<URL & { protocol: "https:" }>();
127
+ });
128
+
129
+ test("z.properties validates symbol keys", () => {
130
+ const sym = Symbol("tag");
131
+ const p = z.properties({ [sym]: z.string() });
132
+ expect(p.safeParse({ [sym]: "ok" }).success).toBe(true);
133
+ expect(p.safeParse({ [sym]: 123 }).error!.issues.map((i) => i.path)).toEqual([[sym]]);
134
+ expect(
135
+ z
136
+ .compile(p)
137
+ .safeParse({ [sym]: 123 })
138
+ .error!.issues.map((i) => i.path)
139
+ ).toEqual([[sym]]);
140
+ });
141
+
142
+ test("z.properties compiled and interpreted agree on a nullish value", () => {
143
+ // a base that permits null reaches the check role with one; the compiled property read must not throw where the runtime reports an issue
144
+ const s = z.any().check(...z.properties({ a: z.string() }));
145
+ const expected = [["invalid_type", []]];
146
+ expect(s.safeParse(null).error!.issues.map((i) => [i.code, i.path])).toEqual(expected);
147
+ expect(
148
+ z
149
+ .compile(s)
150
+ .safeParse(null)
151
+ .error!.issues.map((i) => [i.code, i.path])
152
+ ).toEqual(expected);
153
+ });
154
+
155
+ test("z.properties with a custom when refuses to compile", () => {
156
+ // `when` is not publicly settable, since it gates a check inside the run loop and means nothing to a parsed schema. A hand-built def can still carry one, and compiling it must refuse rather than run the shape unconditionally: a union branch compiles to its own IIFE, so a wrong rejection is absorbed as a branch failure with no fallback.
157
+ const p = new z.core.$ZodProperties({
158
+ type: "properties",
159
+ check: "properties",
160
+ shape: { a: z.literal("x") },
161
+ when: () => false,
162
+ });
163
+ const s = z.union([p as unknown as z.ZodType, z.object({ b: z.string() })]);
164
+ const input = { a: "wrong", b: "hello" };
165
+ expect(z.compile(s).safeParse(input)).toEqual(s.safeParse(input));
166
+ });
167
+
168
+ test("z.properties parses cyclic input", () => {
169
+ // the input is its own output, so it registers as its own memo entry; without that a cycle re-enters forever
170
+ const Node: z.ZodType<{ id: string; next?: unknown }> = z.lazy(() =>
171
+ z.properties({ id: z.string(), next: Node.optional() })
172
+ );
173
+ const cyclic: Record<string, unknown> = { id: "a" };
174
+ cyclic.next = cyclic;
175
+ expect(Node.parse(cyclic)).toBe(cyclic);
176
+ expect(z.compile(Node).parse(cyclic)).toBe(cyclic);
177
+
178
+ // the cycle guard must not swallow a real failure further down
179
+ expect(Node.safeParse({ id: "a", next: { id: 1 } }).error!.issues.map((i) => i.path)).toEqual([["next", "id"]]);
180
+ });
181
+
182
+ test("z.properties asserts forward while encoding", () => {
183
+ // both sides declare the shape's input type, so encoding must still check the value against it; running the children backward would encode them and reject that type
184
+ const codec = z.codec(z.string(), z.number(), { decode: (s) => Number(s), encode: (n) => String(n) });
185
+ const p = z.properties({ a: codec, b: z.string().transform((s) => s.length) });
186
+ const value = { a: "1", b: "hi" };
187
+ expect(z.decode(p, value)).toBe(value);
188
+ expect(z.encode(p, value)).toBe(value);
189
+ expect(z.safeEncode(p, { a: 1, b: "hi" } as never).error!.issues.map((i) => i.path)).toEqual([["a"]]);
190
+ });
191
+
192
+ test("z.properties parses a cyclic callable", () => {
193
+ // property reads work on a function too, so the cycle guard has to recognize one as a reference
194
+ const Fn: z.ZodType<{ self?: unknown }> = z.lazy(() => z.properties({ self: Fn.optional() }));
195
+ const fn = (() => {}) as unknown as Record<string, unknown>;
196
+ fn.self = fn;
197
+ expect(Fn.parse(fn)).toBe(fn);
198
+ });
199
+
200
+ test("z.properties survives shape mutation", () => {
201
+ // key and schema are snapshotted together; caching one and reading the other live paired a stale key with a missing schema
202
+ const shape: Record<string, z.ZodType> = { a: z.string() };
203
+ const p = z.properties(shape);
204
+ expect(p.safeParse({ a: "x" }).success).toBe(true);
205
+ delete shape.a;
206
+ expect(p.safeParse({ a: "x" }).success).toBe(true);
207
+ shape.b = z.string();
208
+ expect(p.safeParse({ a: "x" }).success).toBe(true);
209
+ });
210
+
211
+ test("z.properties JSON Schema describes the input side", () => {
212
+ // the parsed value is the input, so a defaulted key that stays absent must not be required of the output either
213
+ const p = z.properties({ a: z.string().default("x") });
214
+ expect(p.parse({})).toEqual({});
215
+ expect(z.toJSONSchema(p, { io: "output" })).toEqual({
216
+ $schema: "https://json-schema.org/draft/2020-12/schema",
217
+ type: "object",
218
+ properties: { a: { default: "x", type: "string" } },
219
+ });
220
+
221
+ // an output mode emission for a transforming child would describe a value this schema never returns
222
+ const t = z.properties({ a: z.string().transform((s) => s.length) });
223
+ expect(() => z.toJSONSchema(t, { io: "output" })).toThrow(/cannot be represented/);
224
+ expect(z.toJSONSchema(t, { io: "input" })).toMatchObject({ properties: { a: { type: "string" } }, required: ["a"] });
225
+ });
@@ -556,7 +556,7 @@ test("recursive object with .check()", () => {
556
556
  expect(() => Category.parse(invalidData)).toThrow();
557
557
  });
558
558
 
559
- // biome-ignore lint: sadf
559
+ // biome-ignore lint/suspicious/noExportsInTest: unexported it trips noUnusedVariables instead
560
560
  export type RecursiveA = z.ZodUnion<
561
561
  [
562
562
  z.ZodObject<{
@@ -132,3 +132,22 @@ test("z.toJSONSchema() returns StandardJSONSchemaV1", async () => {
132
132
  }
133
133
  `);
134
134
  });
135
+
136
+ test("a failing validate reports the same issues as safeParse, without a ZodError", async () => {
137
+ const schema = z.object({ name: z.string().min(3), age: z.number().refine(async (n) => n > 0) });
138
+ const input = { name: "ab", age: -1 };
139
+ const result = (await schema["~standard"].validate(input)) as { issues: unknown[] };
140
+ expect(result.issues).toEqual((await schema.safeParseAsync(input)).error!.issues);
141
+ expect(result).not.toHaveProperty("error");
142
+ const sync = z.string().min(3)["~standard"].validate("ab") as { issues: unknown[] };
143
+ expect(sync.issues).toEqual(z.string().min(3).safeParse("ab").error!.issues);
144
+ });
145
+
146
+ test("a synchronously throwing check rejects through ~standard instead of throwing", async () => {
147
+ const schema = z.string().refine(() => {
148
+ throw new Error("boom");
149
+ });
150
+ const result = schema["~standard"].validate("abc");
151
+ expect(result).toBeInstanceOf(Promise);
152
+ await expect(result as Promise<unknown>).rejects.toThrow("boom");
153
+ });
@@ -264,6 +264,7 @@ test("base64url validations", () => {
264
264
 
265
265
  const invalidBase64URLStrings = [
266
266
  "w7/Dv8O+w74K", // Has + and / characters (is base64)
267
+ "A", // Invalid length (1 mod 4 carries no whole byte)
267
268
  "12345", // Invalid length (not a multiple of 4 characters when adding allowed number of padding characters)
268
269
  "12345===", // Not padded correctly
269
270
  "!UGF0aWVuY2UgaXMgdGhlIGtleSB0byBzdWNjZXNz", // Invalid character '!'
@@ -291,6 +292,18 @@ test("big base64 and base64url", () => {
291
292
  z.base64().parse(bigbase64);
292
293
  const bigbase64url = randomBytes(1024 * 1024 * 10).toString("base64url");
293
294
  z.base64url().parse(bigbase64url);
295
+
296
+ // template literals compose def.pattern and test it on every parse; the quantified block forms overflowed the regex stack here
297
+ expect(z.templateLiteral(["id-", z.base64()]).safeParse(`id-${bigbase64}`).success).toBe(true);
298
+ expect(z.templateLiteral(["id-", z.base64url()]).safeParse(`id-${bigbase64url}`).success).toBe(true);
299
+ });
300
+
301
+ test("template literal composes base64 without alternation leaks", () => {
302
+ // the ^$| alternation in regexes.base64 used to leak: "AAAA" matched with the prefix dropped and "id-AAAA" was rejected
303
+ const tl = z.templateLiteral(["id-", z.base64()]);
304
+ expect(tl.safeParse("id-AAAA").success).toBe(true);
305
+ expect(tl.safeParse("id-").success).toBe(true);
306
+ expect(tl.safeParse("AAAA").success).toBe(false);
294
307
  });
295
308
 
296
309
  function makeJwt(header: object, payload: object) {
@@ -552,6 +565,18 @@ test("emoji validations", () => {
552
565
  expect(() => emoji.parse("😀 is an emoji")).toThrow();
553
566
  expect(() => emoji.parse("😀stuff")).toThrow();
554
567
  expect(() => emoji.parse("stuff😀")).toThrow();
568
+
569
+ emoji.parse("1\u{FE0F}\u{20E3}"); // keycap sequences stay valid
570
+ emoji.parse("#\u{FE0F}\u{20E3}");
571
+
572
+ // `\p{Emoji_Component}` alone covers ASCII digits, "#", "*", ZWJ, variation selectors and skin tone modifiers; none is an emoji without a base, so a component-only string is not one either
573
+ expect(() => emoji.parse("123")).toThrow();
574
+ expect(() => emoji.parse("#")).toThrow();
575
+ expect(() => emoji.parse("*")).toThrow();
576
+ expect(() => emoji.parse("\u{200D}")).toThrow();
577
+ expect(() => emoji.parse("\u{FE0F}")).toThrow();
578
+ expect(() => emoji.parse("\u{1F3FD}")).toThrow();
579
+ expect(() => emoji.parse("1\u{FE0F}")).toThrow();
555
580
  });
556
581
 
557
582
  test("nanoid", () => {
@@ -565,7 +565,7 @@ test("regexes", () => {
565
565
  `"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"`
566
566
  );
567
567
  expect(email._zod.pattern.source).toMatchInlineSnapshot(
568
- `"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"`
568
+ `"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"`
569
569
  );
570
570
  // expect(ip._zod.pattern.source).toMatchInlineSnapshot(
571
571
  // `"^(^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$)|(^(([a-fA-F0-9]{1,4}:){7}|::([a-fA-F0-9]{1,4}:){0,6}|([a-fA-F0-9]{1,4}:){1}:([a-fA-F0-9]{1,4}:){0,5}|([a-fA-F0-9]{1,4}:){2}:([a-fA-F0-9]{1,4}:){0,4}|([a-fA-F0-9]{1,4}:){3}:([a-fA-F0-9]{1,4}:){0,3}|([a-fA-F0-9]{1,4}:){4}:([a-fA-F0-9]{1,4}:){0,2}|([a-fA-F0-9]{1,4}:){5}:([a-fA-F0-9]{1,4}:){0,1})([a-fA-F0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$)$"`
@@ -778,3 +778,35 @@ test("template literal parsing - failure - issue format", () => {
778
778
  }
779
779
  `);
780
780
  });
781
+
782
+ test("part patterns fold checks through wrappers and unions", () => {
783
+ // a constrained string keeps its bounds under a wrapper and inside a union
784
+ const wrapped = z.templateLiteral(["a-", z.string().min(2).max(3).optional()]);
785
+ expect(wrapped.safeParse("a-").success).toBe(true);
786
+ expect(wrapped.safeParse("a-xy").success).toBe(true);
787
+ expect(wrapped.safeParse("a-x").success).toBe(false);
788
+ expect(wrapped.safeParse("a-wxyz").success).toBe(false);
789
+
790
+ const union = z.templateLiteral(["", z.union([z.string().regex(/^[a-c]+$/), z.number().int()])]);
791
+ expect(union.safeParse("abc").success).toBe(true);
792
+ expect(union.safeParse("12").success).toBe(true);
793
+ expect(union.safeParse("xyz").success).toBe(false);
794
+ expect(union.safeParse("1.5").success).toBe(false);
795
+
796
+ // an empty length range matches nothing rather than building an invalid quantifier
797
+ const empty = z.templateLiteral(["", z.string().min(8).length(5)]);
798
+ expect(empty.safeParse("abcde").success).toBe(false);
799
+
800
+ // lazy resolves its inner schema on the internals, not the def
801
+ const lazy = z.templateLiteral(["", z.lazy(() => z.string().min(2)).optional()]);
802
+ expect(lazy.safeParse("x").success).toBe(false);
803
+ expect(lazy.safeParse("xy").success).toBe(true);
804
+ });
805
+
806
+ test("an embedded email does not constrain the other parts", () => {
807
+ const schema = z.templateLiteral([z.email(), "|", z.string()]);
808
+ expect(schema.safeParse("a@b.cc|a..b").success).toBe(true);
809
+ expect(schema.safeParse("a@b.cc|xy").success).toBe(true);
810
+ expect(schema.safeParse("a..b@b.cc|xy").success).toBe(false);
811
+ expect(schema.safeParse(".a@b.cc|xy").success).toBe(false);
812
+ });
@@ -79,7 +79,7 @@ describe("toJSONSchema", () => {
79
79
  {
80
80
  "$schema": "https://json-schema.org/draft/2020-12/schema",
81
81
  "format": "email",
82
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
82
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
83
83
  "type": "string",
84
84
  }
85
85
  `);
@@ -218,6 +218,15 @@ describe("toJSONSchema", () => {
218
218
  "type": "string",
219
219
  }
220
220
  `);
221
+ expect(z.toJSONSchema(z.base64url())).toMatchInlineSnapshot(`
222
+ {
223
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
224
+ "contentEncoding": "base64url",
225
+ "format": "base64url",
226
+ "pattern": "^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2,3})?$",
227
+ "type": "string",
228
+ }
229
+ `);
221
230
  expect(z.toJSONSchema(z.cuid())).toMatchInlineSnapshot(`
222
231
  {
223
232
  "$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -231,7 +240,7 @@ describe("toJSONSchema", () => {
231
240
  {
232
241
  "$schema": "https://json-schema.org/draft/2020-12/schema",
233
242
  "format": "emoji",
234
- "pattern": "^[\\p{Extended_Pictographic}\\p{Emoji_Component}]+$",
243
+ "pattern": "^(?=[\\s\\S]*[\\p{Extended_Pictographic}\\p{Regional_Indicator}\\u20E3])[\\p{Extended_Pictographic}\\p{Emoji_Component}]+$",
235
244
  "type": "string",
236
245
  }
237
246
  `);
@@ -356,7 +365,7 @@ describe("toJSONSchema", () => {
356
365
  {
357
366
  "$schema": "https://json-schema.org/draft/2020-12/schema",
358
367
  "format": "email",
359
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
368
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
360
369
  "type": "string",
361
370
  }
362
371
  `);
@@ -482,6 +491,30 @@ describe("toJSONSchema", () => {
482
491
  `);
483
492
  });
484
493
 
494
+ test("base64url pattern agrees with parse", () => {
495
+ const schema = z.base64url();
496
+ const pattern = new RegExp(z.toJSONSchema(schema).pattern!);
497
+ // lengths 1 (mod 4) carry no whole byte; the emitted pattern must reject them like parse does
498
+ for (const s of ["", "A", "AA", "AAA", "AAAA", "AAAAA"]) {
499
+ expect(pattern.test(s)).toBe(schema.safeParse(s).success);
500
+ }
501
+ });
502
+
503
+ test("base64 pattern agrees with parse", () => {
504
+ const schema = z.base64();
505
+ const pattern = new RegExp(z.toJSONSchema(schema).pattern!);
506
+ // the runtime def.pattern is lax; the emitted pattern must still carry the block structure parse enforces
507
+ for (const s of ["", "A", "AA", "AAA", "AAAA", "AAAAA", "AA==", "AAA=", "A===", "=", "AAAA=="]) {
508
+ expect(pattern.test(s)).toBe(schema.safeParse(s).success);
509
+ }
510
+ });
511
+
512
+ test("looseRecord with a format key emits the exact pattern", () => {
513
+ // recordProcessor reads bag.patterns directly; it must apply the same lax-to-exact swap as stringProcessor
514
+ const json = z.toJSONSchema(z.looseRecord(z.base64url(), z.number()));
515
+ expect(Object.keys(json.patternProperties!)).toEqual([z.regexes.base64url.source]);
516
+ });
517
+
485
518
  test("string patterns", () => {
486
519
  expect(
487
520
  z.toJSONSchema(
@@ -724,6 +757,117 @@ describe("toJSONSchema", () => {
724
757
  `);
725
758
  });
726
759
 
760
+ test("number constraints order independence with int()", () => {
761
+ // .int() applied last should not overwrite the tighter bounds .min()/.max() already stored
762
+ expect(z.toJSONSchema(z.number().int().min(0).max(23))).toMatchInlineSnapshot(`
763
+ {
764
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
765
+ "maximum": 23,
766
+ "minimum": 0,
767
+ "type": "integer",
768
+ }
769
+ `);
770
+ expect(z.toJSONSchema(z.number().min(0).max(23).int())).toMatchInlineSnapshot(`
771
+ {
772
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
773
+ "maximum": 23,
774
+ "minimum": 0,
775
+ "type": "integer",
776
+ }
777
+ `);
778
+ });
779
+
780
+ test("chained multipleOf divisors emit the whole conjunction", () => {
781
+ // runtime rejects 4 (not a multiple of 3), so the emitted schema must too
782
+ expect(z.toJSONSchema(z.number().multipleOf(2).multipleOf(3))).toMatchInlineSnapshot(`
783
+ {
784
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
785
+ "allOf": [
786
+ {
787
+ "multipleOf": 3,
788
+ },
789
+ ],
790
+ "multipleOf": 2,
791
+ "type": "number",
792
+ }
793
+ `);
794
+ });
795
+
796
+ test("length() does not overwrite a tighter earlier bound", () => {
797
+ // min(8) + length(5) matches nothing at runtime; the emitted bounds stay honest about that
798
+ expect(z.toJSONSchema(z.string().min(8).length(5))).toMatchInlineSnapshot(`
799
+ {
800
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
801
+ "maxLength": 5,
802
+ "minLength": 8,
803
+ "type": "string",
804
+ }
805
+ `);
806
+ });
807
+
808
+ test("an integer format keeps the emitted type integer whatever format lands last", () => {
809
+ // runtime rejects 1.5 in both orders, so both must emit type integer
810
+ const after = z.number().int().check(z.float32());
811
+ expect(after.safeParse(1.5).success).toBe(false);
812
+ expect(z.toJSONSchema(after).type).toBe("integer");
813
+ expect(z.toJSONSchema(z.number().check(z.float32()).int()).type).toBe("integer");
814
+ });
815
+
816
+ test("disjoint mime checks emit a false schema, not an unconstrained file", () => {
817
+ const file = z.file().mime("image/png").mime("text/plain");
818
+ expect(z.toJSONSchema(file)).toMatchInlineSnapshot(`
819
+ {
820
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
821
+ "contentEncoding": "binary",
822
+ "format": "binary",
823
+ "not": {},
824
+ "type": "string",
825
+ }
826
+ `);
827
+ });
828
+
829
+ test("a custom mime narrowing via onattach intersects with built-in mime checks", () => {
830
+ const check = {
831
+ _zod: {
832
+ def: { check: "custom_mime" },
833
+ onattach: [
834
+ (inst: any) => {
835
+ inst._zod.bag.mime = ["text/plain"];
836
+ },
837
+ ],
838
+ check: () => {},
839
+ },
840
+ };
841
+ const file = z
842
+ .file()
843
+ .mime(["image/png", "text/plain"])
844
+ .check(check as any);
845
+ expect(z.toJSONSchema(file).contentMediaType).toBe("text/plain");
846
+ });
847
+
848
+ test("a custom check contributing via onattach still lands", () => {
849
+ // third-party checks have no converter handler; their bag writes are merged in as a fallback
850
+ const check = {
851
+ _zod: {
852
+ def: { check: "custom_range" },
853
+ onattach: [
854
+ (inst: any) => {
855
+ const bag = inst._zod.bag;
856
+ if (bag.minimum === undefined || 5 > bag.minimum) bag.minimum = 5;
857
+ },
858
+ ],
859
+ check: () => {},
860
+ },
861
+ };
862
+ expect(z.toJSONSchema(z.number().check(check as any))).toMatchInlineSnapshot(`
863
+ {
864
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
865
+ "minimum": 5,
866
+ "type": "number",
867
+ }
868
+ `);
869
+ });
870
+
727
871
  test("target normalization draft-04 and draft-07", () => {
728
872
  // Test that both old (draft-4, draft-7) and new (draft-04, draft-07) target formats work
729
873