zod 4.6.3 → 4.6.5

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 (232) hide show
  1. package/package.json +1 -1
  2. package/src/v4/classic/schemas.ts +11 -5
  3. package/src/v4/classic/tests/assignability.test.ts +1 -0
  4. package/src/v4/classic/tests/instance-footprint.test.ts +22 -0
  5. package/src/v4/classic/tests/number.test.ts +39 -0
  6. package/src/v4/classic/tests/properties.test.ts +19 -0
  7. package/src/v4/classic/tests/string.test.ts +70 -1
  8. package/src/v4/classic/tests/to-json-schema.test.ts +87 -35
  9. package/src/v4/core/api.ts +17 -22
  10. package/src/v4/core/compile.ts +27 -6
  11. package/src/v4/core/core.ts +1 -3
  12. package/src/v4/core/json-schema-processors.ts +9 -13
  13. package/src/v4/core/regexes.ts +4 -0
  14. package/src/v4/core/schemas.ts +35 -11
  15. package/src/v4/core/tests/compile-differential.test.ts +35 -0
  16. package/src/v4/core/tests/compile.test.ts +177 -0
  17. package/src/v4/core/tests/url-no-canparse.test.ts +118 -16
  18. package/src/v4/core/versions.ts +1 -1
  19. package/src/v4/locales/ar.ts +1 -0
  20. package/src/v4/locales/az.ts +1 -0
  21. package/src/v4/locales/be.ts +1 -0
  22. package/src/v4/locales/bg.ts +1 -0
  23. package/src/v4/locales/bn.ts +1 -0
  24. package/src/v4/locales/ca.ts +1 -0
  25. package/src/v4/locales/ckb.ts +1 -0
  26. package/src/v4/locales/cs.ts +1 -0
  27. package/src/v4/locales/da.ts +1 -0
  28. package/src/v4/locales/de.ts +1 -0
  29. package/src/v4/locales/el.ts +1 -0
  30. package/src/v4/locales/en.ts +1 -0
  31. package/src/v4/locales/eo.ts +1 -0
  32. package/src/v4/locales/es.ts +1 -0
  33. package/src/v4/locales/fa.ts +1 -0
  34. package/src/v4/locales/fi.ts +1 -0
  35. package/src/v4/locales/fr-CA.ts +1 -0
  36. package/src/v4/locales/fr.ts +1 -0
  37. package/src/v4/locales/gu.ts +1 -0
  38. package/src/v4/locales/he.ts +1 -0
  39. package/src/v4/locales/hi.ts +1 -0
  40. package/src/v4/locales/hr.ts +1 -0
  41. package/src/v4/locales/hu.ts +1 -0
  42. package/src/v4/locales/hy.ts +1 -0
  43. package/src/v4/locales/id.ts +1 -0
  44. package/src/v4/locales/is.ts +1 -0
  45. package/src/v4/locales/it.ts +1 -0
  46. package/src/v4/locales/ja.ts +1 -0
  47. package/src/v4/locales/ka.ts +1 -0
  48. package/src/v4/locales/km.ts +1 -0
  49. package/src/v4/locales/kn.ts +1 -0
  50. package/src/v4/locales/ko.ts +1 -0
  51. package/src/v4/locales/lt.ts +1 -0
  52. package/src/v4/locales/mk.ts +1 -0
  53. package/src/v4/locales/ms.ts +1 -0
  54. package/src/v4/locales/ne.ts +1 -0
  55. package/src/v4/locales/nl.ts +1 -0
  56. package/src/v4/locales/nn.ts +1 -0
  57. package/src/v4/locales/no.ts +1 -0
  58. package/src/v4/locales/ota.ts +1 -0
  59. package/src/v4/locales/pl.ts +1 -0
  60. package/src/v4/locales/ps.ts +1 -0
  61. package/src/v4/locales/pt-BR.ts +1 -0
  62. package/src/v4/locales/pt.ts +1 -0
  63. package/src/v4/locales/ro.ts +1 -0
  64. package/src/v4/locales/ru.ts +1 -0
  65. package/src/v4/locales/sk.ts +1 -0
  66. package/src/v4/locales/sl.ts +1 -0
  67. package/src/v4/locales/sv.ts +1 -0
  68. package/src/v4/locales/ta.ts +1 -0
  69. package/src/v4/locales/tg.ts +1 -0
  70. package/src/v4/locales/th.ts +1 -0
  71. package/src/v4/locales/tk.ts +1 -0
  72. package/src/v4/locales/tr.ts +1 -0
  73. package/src/v4/locales/uk.ts +1 -0
  74. package/src/v4/locales/ur.ts +1 -0
  75. package/src/v4/locales/uz.ts +1 -0
  76. package/src/v4/locales/vi.ts +1 -0
  77. package/src/v4/locales/yo.ts +1 -0
  78. package/src/v4/locales/zh-CN.ts +1 -0
  79. package/src/v4/locales/zh-TW.ts +1 -0
  80. package/src/v4/mini/schemas.ts +12 -4
  81. package/src/v4/mini/tests/index.test.ts +56 -0
  82. package/src/v4/mini/tests/string.test.ts +7 -0
  83. package/v4/classic/schemas.cjs +9 -4
  84. package/v4/classic/schemas.d.cts +4 -1
  85. package/v4/classic/schemas.d.ts +4 -1
  86. package/v4/classic/schemas.js +8 -4
  87. package/v4/core/api.cjs +9 -20
  88. package/v4/core/api.d.cts +6 -2
  89. package/v4/core/api.d.ts +6 -2
  90. package/v4/core/api.js +9 -20
  91. package/v4/core/compile.cjs +7 -6
  92. package/v4/core/compile.js +8 -7
  93. package/v4/core/core.cjs +1 -1
  94. package/v4/core/core.js +1 -1
  95. package/v4/core/json-schema-processors.cjs +7 -10
  96. package/v4/core/json-schema-processors.js +7 -10
  97. package/v4/core/regexes.cjs +4 -2
  98. package/v4/core/regexes.d.cts +1 -0
  99. package/v4/core/regexes.d.ts +1 -0
  100. package/v4/core/regexes.js +2 -0
  101. package/v4/core/schemas.cjs +32 -11
  102. package/v4/core/schemas.d.cts +5 -3
  103. package/v4/core/schemas.d.ts +5 -3
  104. package/v4/core/schemas.js +30 -11
  105. package/v4/core/versions.cjs +1 -1
  106. package/v4/core/versions.js +1 -1
  107. package/v4/locales/ar.cjs +1 -0
  108. package/v4/locales/ar.js +1 -0
  109. package/v4/locales/az.cjs +1 -0
  110. package/v4/locales/az.js +1 -0
  111. package/v4/locales/be.cjs +1 -0
  112. package/v4/locales/be.js +1 -0
  113. package/v4/locales/bg.cjs +1 -0
  114. package/v4/locales/bg.js +1 -0
  115. package/v4/locales/bn.cjs +1 -0
  116. package/v4/locales/bn.js +1 -0
  117. package/v4/locales/ca.cjs +1 -0
  118. package/v4/locales/ca.js +1 -0
  119. package/v4/locales/ckb.cjs +1 -0
  120. package/v4/locales/ckb.js +1 -0
  121. package/v4/locales/cs.cjs +1 -0
  122. package/v4/locales/cs.js +1 -0
  123. package/v4/locales/da.cjs +1 -0
  124. package/v4/locales/da.js +1 -0
  125. package/v4/locales/de.cjs +1 -0
  126. package/v4/locales/de.js +1 -0
  127. package/v4/locales/el.cjs +1 -0
  128. package/v4/locales/el.js +1 -0
  129. package/v4/locales/en.cjs +1 -0
  130. package/v4/locales/en.js +1 -0
  131. package/v4/locales/eo.cjs +1 -0
  132. package/v4/locales/eo.js +1 -0
  133. package/v4/locales/es.cjs +1 -0
  134. package/v4/locales/es.js +1 -0
  135. package/v4/locales/fa.cjs +1 -0
  136. package/v4/locales/fa.js +1 -0
  137. package/v4/locales/fi.cjs +1 -0
  138. package/v4/locales/fi.js +1 -0
  139. package/v4/locales/fr-CA.cjs +1 -0
  140. package/v4/locales/fr-CA.js +1 -0
  141. package/v4/locales/fr.cjs +1 -0
  142. package/v4/locales/fr.js +1 -0
  143. package/v4/locales/gu.cjs +1 -0
  144. package/v4/locales/gu.js +1 -0
  145. package/v4/locales/he.cjs +1 -0
  146. package/v4/locales/he.js +1 -0
  147. package/v4/locales/hi.cjs +1 -0
  148. package/v4/locales/hi.js +1 -0
  149. package/v4/locales/hr.cjs +1 -0
  150. package/v4/locales/hr.js +1 -0
  151. package/v4/locales/hu.cjs +1 -0
  152. package/v4/locales/hu.js +1 -0
  153. package/v4/locales/hy.cjs +1 -0
  154. package/v4/locales/hy.js +1 -0
  155. package/v4/locales/id.cjs +1 -0
  156. package/v4/locales/id.js +1 -0
  157. package/v4/locales/is.cjs +1 -0
  158. package/v4/locales/is.js +1 -0
  159. package/v4/locales/it.cjs +1 -0
  160. package/v4/locales/it.js +1 -0
  161. package/v4/locales/ja.cjs +1 -0
  162. package/v4/locales/ja.js +1 -0
  163. package/v4/locales/ka.cjs +1 -0
  164. package/v4/locales/ka.js +1 -0
  165. package/v4/locales/km.cjs +1 -0
  166. package/v4/locales/km.js +1 -0
  167. package/v4/locales/kn.cjs +1 -0
  168. package/v4/locales/kn.js +1 -0
  169. package/v4/locales/ko.cjs +1 -0
  170. package/v4/locales/ko.js +1 -0
  171. package/v4/locales/lt.cjs +1 -0
  172. package/v4/locales/lt.js +1 -0
  173. package/v4/locales/mk.cjs +1 -0
  174. package/v4/locales/mk.js +1 -0
  175. package/v4/locales/ms.cjs +1 -0
  176. package/v4/locales/ms.js +1 -0
  177. package/v4/locales/ne.cjs +1 -0
  178. package/v4/locales/ne.js +1 -0
  179. package/v4/locales/nl.cjs +1 -0
  180. package/v4/locales/nl.js +1 -0
  181. package/v4/locales/nn.cjs +1 -0
  182. package/v4/locales/nn.js +1 -0
  183. package/v4/locales/no.cjs +1 -0
  184. package/v4/locales/no.js +1 -0
  185. package/v4/locales/ota.cjs +1 -0
  186. package/v4/locales/ota.js +1 -0
  187. package/v4/locales/pl.cjs +1 -0
  188. package/v4/locales/pl.js +1 -0
  189. package/v4/locales/ps.cjs +1 -0
  190. package/v4/locales/ps.js +1 -0
  191. package/v4/locales/pt-BR.cjs +1 -0
  192. package/v4/locales/pt-BR.js +1 -0
  193. package/v4/locales/pt.cjs +1 -0
  194. package/v4/locales/pt.js +1 -0
  195. package/v4/locales/ro.cjs +1 -0
  196. package/v4/locales/ro.js +1 -0
  197. package/v4/locales/ru.cjs +1 -0
  198. package/v4/locales/ru.js +1 -0
  199. package/v4/locales/sk.cjs +1 -0
  200. package/v4/locales/sk.js +1 -0
  201. package/v4/locales/sl.cjs +1 -0
  202. package/v4/locales/sl.js +1 -0
  203. package/v4/locales/sv.cjs +1 -0
  204. package/v4/locales/sv.js +1 -0
  205. package/v4/locales/ta.cjs +1 -0
  206. package/v4/locales/ta.js +1 -0
  207. package/v4/locales/tg.cjs +1 -0
  208. package/v4/locales/tg.js +1 -0
  209. package/v4/locales/th.cjs +1 -0
  210. package/v4/locales/th.js +1 -0
  211. package/v4/locales/tk.cjs +1 -0
  212. package/v4/locales/tk.js +1 -0
  213. package/v4/locales/tr.cjs +1 -0
  214. package/v4/locales/tr.js +1 -0
  215. package/v4/locales/uk.cjs +1 -0
  216. package/v4/locales/uk.js +1 -0
  217. package/v4/locales/ur.cjs +1 -0
  218. package/v4/locales/ur.js +1 -0
  219. package/v4/locales/uz.cjs +1 -0
  220. package/v4/locales/uz.js +1 -0
  221. package/v4/locales/vi.cjs +1 -0
  222. package/v4/locales/vi.js +1 -0
  223. package/v4/locales/yo.cjs +1 -0
  224. package/v4/locales/yo.js +1 -0
  225. package/v4/locales/zh-CN.cjs +1 -0
  226. package/v4/locales/zh-CN.js +1 -0
  227. package/v4/locales/zh-TW.cjs +1 -0
  228. package/v4/locales/zh-TW.js +1 -0
  229. package/v4/mini/schemas.cjs +10 -4
  230. package/v4/mini/schemas.d.cts +1 -0
  231. package/v4/mini/schemas.d.ts +1 -0
  232. package/v4/mini/schemas.js +9 -4
@@ -522,10 +522,30 @@ export interface $ZodURL extends $ZodType {
522
522
 
523
523
  /** The `://` guard rejected the input before the URL constructor saw it. */
524
524
  export const URL_BAD_FORMAT = 1;
525
- /** The URL constructor rejected the input. */
525
+ /** The URL parser rejected the input. */
526
526
  export const URL_UNPARSEABLE = 2;
527
527
 
528
- /** Parses a URL for `$ZodURL`, applying the one guard the URL constructor cannot express. Returns the parsed URL, or a code naming the stage that rejected it — the runtime needs that distinction to pick an issue note, and compiled code only needs to know it is not a URL. */
528
+ export function canParseURL(input: string): boolean {
529
+ try {
530
+ if (typeof URL !== "undefined" && typeof URL.canParse === "function") return URL.canParse(input);
531
+ new URL(input);
532
+ return true;
533
+ } catch {
534
+ return false;
535
+ }
536
+ }
537
+
538
+ export function validateURL(
539
+ trimmed: string,
540
+ def: Pick<$ZodURLDef, "protocol" | "hostname" | "normalize">
541
+ ): URL | true | typeof URL_BAD_FORMAT | typeof URL_UNPARSEABLE {
542
+ if (!("normalize" in def) && !("hostname" in def) && !("protocol" in def)) {
543
+ return canParseURL(trimmed) || URL_UNPARSEABLE;
544
+ }
545
+ return parseURLObject(trimmed, def);
546
+ }
547
+
548
+ /** Parses a URL while preserving the non-normalizing HTTP guard. */
529
549
  export function parseURLObject(
530
550
  trimmed: string,
531
551
  def: Pick<$ZodURLDef, "protocol" | "normalize">
@@ -536,6 +556,10 @@ export function parseURLObject(
536
556
  }
537
557
 
538
558
  try {
559
+ if (typeof URL !== "undefined") {
560
+ const URLStatic = URL as typeof URL & { parse?: (input: string) => URL | null };
561
+ if (typeof URLStatic.parse === "function") return URLStatic.parse(trimmed) ?? URL_UNPARSEABLE;
562
+ }
539
563
  // @ts-ignore
540
564
  return new URL(trimmed);
541
565
  } catch {
@@ -566,7 +590,7 @@ export const $ZodURL: core.$constructor<$ZodURL> = /*@__PURE__*/ core.$construct
566
590
  try {
567
591
  // Trim whitespace from input
568
592
  const trimmed = payload.value.trim();
569
- const url = parseURLObject(trimmed, def);
593
+ const url = validateURL(trimmed, def);
570
594
 
571
595
  if (url === URL_BAD_FORMAT) {
572
596
  payload.issues.push({
@@ -591,6 +615,11 @@ export const $ZodURL: core.$constructor<$ZodURL> = /*@__PURE__*/ core.$construct
591
615
  return;
592
616
  }
593
617
 
618
+ if (url === true) {
619
+ payload.value = stripTabAndNewline(trimmed);
620
+ return;
621
+ }
622
+
594
623
  if (def.hostname && !urlHostnameOk(url, def.hostname)) {
595
624
  payload.issues.push({
596
625
  code: "invalid_format",
@@ -885,13 +914,7 @@ const ipv6Alphabet = /^[0-9a-fA-F:.]+$/;
885
914
 
886
915
  export function isValidIPv6(value: string): boolean {
887
916
  if (!ipv6Alphabet.test(value)) return false;
888
- try {
889
- // @ts-ignore
890
- new URL(`http://[${value}]`);
891
- return true;
892
- } catch {
893
- return false;
894
- }
917
+ return canParseURL(`http://[${value}]`);
895
918
  }
896
919
 
897
920
  export const $ZodIPv6: core.$constructor<$ZodIPv6> = /*@__PURE__*/ core.$constructor("$ZodIPv6", (inst, def): void => {
@@ -5165,4 +5188,5 @@ export type $ZodStringFormatTypes =
5165
5188
  | $ZodJWT
5166
5189
  | $ZodCustomStringFormat<"hex">
5167
5190
  | $ZodCustomStringFormat<util.HashFormat>
5168
- | $ZodCustomStringFormat<"hostname">;
5191
+ | $ZodCustomStringFormat<"hostname">
5192
+ | $ZodCustomStringFormat<"currency_code">;
@@ -677,6 +677,41 @@ test("url options match the runtime across the whole option matrix", () => {
677
677
  for (const schema of schemas) differential(schema, inputs);
678
678
  });
679
679
 
680
+ test("assert-only URL generation skips unused output without skipping chained transforms", () => {
681
+ const sanitized = "https://example.com";
682
+ const withNewline = "https://exa\nmple.com";
683
+ differential(z.url().length(sanitized.length), [sanitized, withNewline, "https://example.com/long"]);
684
+ differential(z.string().url().length(sanitized.length), [sanitized, withNewline, "https://example.com/long"]);
685
+
686
+ const URLStatic = URL as typeof URL & { parse: (input: string) => URL | null };
687
+ const descriptor = Object.getOwnPropertyDescriptor(URLStatic, "parse")!;
688
+ const marker = new Error("href read");
689
+ Object.defineProperty(URLStatic, "parse", {
690
+ configurable: true,
691
+ value: () =>
692
+ Object.create(URL.prototype, {
693
+ href: {
694
+ get() {
695
+ throw marker;
696
+ },
697
+ },
698
+ }) as URL,
699
+ });
700
+ try {
701
+ const schema = z.url({ normalize: true });
702
+ const parser = compileFn(schema);
703
+ const validator = compileFn(schema, { assertOnly: true });
704
+ expect(validator("https://example.com")).toBe(true);
705
+ expect(z.validate(compile(z.url({ normalize: true })), "https://example.com")).toBe(true);
706
+ expect(z.validate(compile(z.object({ url: z.url({ normalize: true }) })), { url: "https://example.com" })).toBe(
707
+ true
708
+ );
709
+ expect(() => parser("https://example.com")).toThrow(marker);
710
+ } finally {
711
+ Object.defineProperty(URLStatic, "parse", descriptor);
712
+ }
713
+ });
714
+
680
715
  test("empty strict object compiles", () => {
681
716
  // With no keys the unknown-key condition is empty, and `if () return INVALID;` is a syntax error the single top-level `new Function` rejects — too late for compileChild to island, so the whole tree lost its fast path.
682
717
  differential(z.strictObject({}), [{}, { a: 1 }, Object.create({ inherited: 1 })]);
@@ -1787,6 +1787,183 @@ test("compiled records walk own enumerable keys without Reflect.ownKeys", () =>
1787
1787
  expect(compile(plain).parse(revealing())).toStrictEqual({ a: 1, b: 2 });
1788
1788
  });
1789
1789
 
1790
+ test("validate honors custom checks composed with format traits", () => {
1791
+ const postProcessor = z.core.globalConfig.postProcessor;
1792
+ z.core.globalConfig.postProcessor = undefined;
1793
+ try {
1794
+ const CustomEmail = z.core.$constructor<z.core.$ZodEmail>("CustomEmail", (inst, def) => {
1795
+ inst._zod.check = (payload) => {
1796
+ if (payload.value !== "custom") payload.issues.push({ code: "custom", input: payload.value });
1797
+ };
1798
+ z.core.$ZodEmail.init(inst, def);
1799
+ });
1800
+ const schema = new CustomEmail({ type: "string", format: "email", check: "string_format" });
1801
+ for (const [input, valid] of [
1802
+ ["custom", true],
1803
+ ["test@example.com", false],
1804
+ [42, false],
1805
+ ] as const) {
1806
+ expect(z.safeParse(schema, input).success).toBe(valid);
1807
+ expect(z.validate(schema, input)).toBe(valid);
1808
+ }
1809
+ } finally {
1810
+ z.core.globalConfig.postProcessor = postProcessor;
1811
+ }
1812
+ });
1813
+
1814
+ test("validate honors standalone format checks", () => {
1815
+ const cases = [
1816
+ [z.iso.datetime(), "2021-01-01T00:00:00Z"],
1817
+ [z.iso.date(), "2021-01-01"],
1818
+ [z.iso.time(), "00:00:00"],
1819
+ [z.iso.duration(), "P1Y2M3DT4H5M6S"],
1820
+ [z.email(), "test@example.com"],
1821
+ [z.uuid(), "20354d7a-e4fe-47af-8ff6-187bca92f3f9"],
1822
+ [z.ipv4(), "192.168.0.1"],
1823
+ ] as const;
1824
+
1825
+ for (const [schema, valid] of cases) {
1826
+ expect(schema._zod.traits.has("$ZodStringFormat")).toBe(true);
1827
+ expect(schema instanceof z.core.$ZodStringFormat).toBe(true);
1828
+ expect(z.validate(schema, valid)).toBe(true);
1829
+ expect(z.validate(schema, "invalid")).toBe(false);
1830
+ expect(z.validate(schema, 1)).toBe(false);
1831
+ }
1832
+
1833
+ const source = z.email();
1834
+ const refined = source.refine((value) => value.startsWith("x"));
1835
+ const asyncRefined = source.refine(async () => true);
1836
+ const coercedDef = Object.assign(Object.create({ coerce: true }), source._zod.def);
1837
+ const coerced = source.clone(coercedDef);
1838
+ const checkedDef = Object.assign(Object.create({ checks: refined._zod.def.checks }), source._zod.def);
1839
+ const inheritedChecked = source.clone(checkedDef);
1840
+ expect(z.validate(refined, "test@example.com")).toBe(false);
1841
+ expect(z.validate(refined, "x@example.com")).toBe(true);
1842
+ expect(z.validate(inheritedChecked, "test@example.com")).toBe(false);
1843
+ expect(() => z.validate(asyncRefined, "test@example.com")).toThrow($ZodAsyncError);
1844
+ expect(z.validate(coerced, { toString: () => "test@example.com" })).toBe(true);
1845
+ });
1846
+
1847
+ test("runtime validate preserves contexts, getters, errors, and regex state", () => {
1848
+ const postProcessor = z.core.globalConfig.postProcessor;
1849
+ z.core.globalConfig.postProcessor = undefined;
1850
+ try {
1851
+ const source = z.email();
1852
+ expect(z.validate(source, "invalid", { skipChecks: true } as any)).toBe(true);
1853
+
1854
+ let whenReads = 0;
1855
+ const whenProto = Object.defineProperty({}, "when", {
1856
+ enumerable: true,
1857
+ get() {
1858
+ whenReads++;
1859
+ return () => false;
1860
+ },
1861
+ });
1862
+ const whenDef = Object.assign(Object.create(whenProto), source._zod.def);
1863
+ const gated = source.clone(whenDef as typeof source._zod.def);
1864
+ expect(whenReads).toBe(0);
1865
+ expect(z.validate(gated, "invalid")).toBe(true);
1866
+ expect(whenReads).toBeGreaterThan(0);
1867
+
1868
+ let errorReads = 0;
1869
+ const errorDef = Object.defineProperties({}, Object.getOwnPropertyDescriptors(source._zod.def));
1870
+ Object.defineProperty(errorDef, "error", {
1871
+ enumerable: true,
1872
+ get() {
1873
+ errorReads++;
1874
+ return () => "custom";
1875
+ },
1876
+ });
1877
+ const customError = source.clone(errorDef as typeof source._zod.def);
1878
+ expect(z.validate(customError, "invalid")).toBe(false);
1879
+ expect(errorReads).toBe(0);
1880
+ const result = customError.safeParse("invalid");
1881
+ expect(errorReads).toBe(0);
1882
+ if (result.success) expect.unreachable();
1883
+ expect(result.error.issues[0]?.message).toBe("custom");
1884
+ expect(errorReads).toBeGreaterThan(0);
1885
+
1886
+ const pattern = /^a$/g;
1887
+ const cloned = source.clone({ ...source._zod.def, pattern });
1888
+ for (let i = 0; i < 3; i++) expect(z.validate(cloned, "a")).toBe(true);
1889
+ expect(z.validate(cloned, "b")).toBe(false);
1890
+ expect(pattern.lastIndex).toBe(0);
1891
+ const replacement = /^b$/g;
1892
+ cloned._zod.def.pattern = replacement;
1893
+ expect(z.validate(cloned, "a")).toBe(false);
1894
+ expect(z.validate(cloned, "b")).toBe(true);
1895
+ } finally {
1896
+ z.core.globalConfig.postProcessor = postProcessor;
1897
+ }
1898
+ });
1899
+
1900
+ test("validate preserves live pattern accessor reads", () => {
1901
+ const postProcessor = z.core.globalConfig.postProcessor;
1902
+ z.core.globalConfig.postProcessor = undefined;
1903
+ try {
1904
+ const schema = z.email();
1905
+ let reads = 0;
1906
+ Object.defineProperty(schema.def, "pattern", {
1907
+ get: () => (++reads % 2 === 1 ? /^a$/ : /^b$/),
1908
+ });
1909
+ expect(schema.safeParse("b").success).toBe(true);
1910
+ expect(reads).toBe(2);
1911
+ reads = 0;
1912
+ expect(z.validate(schema, "b")).toBe(true);
1913
+ expect(reads).toBe(2);
1914
+ } finally {
1915
+ z.core.globalConfig.postProcessor = postProcessor;
1916
+ }
1917
+ });
1918
+
1919
+ test.each(["when", "coerce"] as const)("runtime validate honors live %s options", (option) => {
1920
+ const postProcessor = z.core.globalConfig.postProcessor;
1921
+ z.core.globalConfig.postProcessor = undefined;
1922
+ try {
1923
+ for (const inherited of [false, true]) {
1924
+ const schema = z.email();
1925
+ const input = option === "when" ? "invalid" : { toString: () => "test@example.com" };
1926
+ expect(z.validate(schema, input)).toBe(false);
1927
+
1928
+ const target = inherited ? Object.create(Object.getPrototypeOf(schema.def)) : schema.def;
1929
+ let reads = 0;
1930
+ Object.defineProperty(target, option, {
1931
+ get() {
1932
+ reads++;
1933
+ return option === "when" ? () => false : true;
1934
+ },
1935
+ });
1936
+ if (inherited) Object.setPrototypeOf(schema.def, target);
1937
+ expect(reads).toBe(0);
1938
+ expect(z.validate(schema, input)).toBe(true);
1939
+ expect(reads).toBeGreaterThan(0);
1940
+ expect(schema.safeParse(input).success).toBe(true);
1941
+ }
1942
+ } finally {
1943
+ z.core.globalConfig.postProcessor = postProcessor;
1944
+ }
1945
+ });
1946
+
1947
+ test("validate uses compiled validators and runtime fallbacks", () => {
1948
+ const compiled = compile(z.email());
1949
+ expect((compiled._zod.bag as { validator?: unknown }).validator).toBeTypeOf("function");
1950
+ expect(z.validate(compiled, "test@example.com")).toBe(true);
1951
+ expect(z.validate(compiled, "invalid")).toBe(false);
1952
+ expect(z.validate(compiled, "invalid", { skipChecks: true } as any)).toBe(true);
1953
+
1954
+ const source = z.email();
1955
+ const installed = withParser(source, () => INVALID);
1956
+ const bag = installed._zod.bag as { fallbackRun: typeof source._zod.run };
1957
+ const originalRun = bag.fallbackRun;
1958
+ let fallbackRuns = 0;
1959
+ bag.fallbackRun = (payload, ctx) => {
1960
+ fallbackRuns++;
1961
+ return originalRun(payload, ctx);
1962
+ };
1963
+ expect(z.validate(installed, "invalid")).toBe(false);
1964
+ expect(fallbackRuns).toBeGreaterThan(0);
1965
+ });
1966
+
1790
1967
  // withParser: a parser Zod did not generate, installed on the same wrapper compile() uses.
1791
1968
 
1792
1969
  // tags its output so a test fails loudly if the runtime ran instead of the supplied parser
@@ -1,31 +1,133 @@
1
1
  import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
2
 
3
- // schemas.ts resolves URL.canParse on first use, not at module load. Resetting the module registry gives the re-imported copy a fresh, unresolved `urlCanParseImpl`, so deleting URL.canParse before the first parse still selects the inline fallback branch (URL.canParse may be undefined on older runtimes).
4
- describe("z.url() with URL.canParse absent", () => {
5
- const original = URL.canParse;
3
+ const URLDescriptor = Object.getOwnPropertyDescriptor(globalThis, "URL")!;
4
+ const NativeURL = globalThis.URL;
5
+ const canParseDescriptor = Object.getOwnPropertyDescriptor(NativeURL, "canParse")!;
6
+ const parseDescriptor = Object.getOwnPropertyDescriptor(NativeURL, "parse")!;
6
7
 
8
+ function restoreURL() {
9
+ Object.defineProperty(globalThis, "URL", URLDescriptor);
10
+ Object.defineProperty(NativeURL, "canParse", canParseDescriptor);
11
+ Object.defineProperty(NativeURL, "parse", parseDescriptor);
12
+ }
13
+
14
+ describe("URL parser fallbacks", () => {
7
15
  beforeEach(() => {
16
+ restoreURL();
8
17
  vi.resetModules();
9
- // @ts-expect-error force the fallback branch (URL.canParse may be undefined on older runtimes)
10
- delete URL.canParse;
11
18
  });
12
19
 
13
- afterEach(() => {
14
- URL.canParse = original;
20
+ afterEach(restoreURL);
21
+
22
+ test("uses the boolean and object APIs on modern runtimes", async () => {
23
+ const canParse = vi.spyOn(NativeURL, "canParse");
24
+ const URLStatic = NativeURL as typeof URL & { parse: (input: string) => URL | null };
25
+ const parse = vi.spyOn(URLStatic, "parse");
26
+ const z = await import("../../index.js");
27
+
28
+ expect(z.validate(z.url(), "https://example.com")).toBe(true);
29
+ expect(canParse).toHaveBeenCalledOnce();
30
+ expect(parse).not.toHaveBeenCalled();
31
+
32
+ canParse.mockClear();
33
+ expect(z.validate(z.url({ normalize: true }), "https://example.com")).toBe(true);
34
+ expect(parse).toHaveBeenCalledOnce();
35
+ expect(canParse).not.toHaveBeenCalled();
36
+
37
+ parse.mockClear();
38
+ expect(z.validate(z.ipv6(), "2001:db8::1")).toBe(true);
39
+ expect(canParse).toHaveBeenCalledOnce();
40
+ expect(parse).not.toHaveBeenCalled();
15
41
  });
16
42
 
17
- test("z.url() validates correctly without URL.canParse (runtime)", async () => {
18
- expect(URL.canParse).toBeUndefined();
43
+ test("URL fast-path selection preserves live option getter reads", async () => {
19
44
  const z = await import("../../index.js");
20
- expect(z.url().safeParse("https://example.com").success).toBe(true);
21
- expect(z.url().safeParse("not a url").success).toBe(false);
45
+ const postProcessor = z.core.globalConfig.postProcessor;
46
+ z.core.globalConfig.postProcessor = undefined;
47
+ try {
48
+ for (const inherited of [false, true]) {
49
+ const schema = z.url();
50
+ const target = inherited ? Object.create(Object.getPrototypeOf(schema.def)) : schema.def;
51
+ let reads = 0;
52
+ Object.defineProperty(target, "hostname", {
53
+ get: () => (++reads === 1 ? /^example\.com$/ : undefined),
54
+ });
55
+ if (inherited) Object.setPrototypeOf(schema.def, target);
56
+ expect(z.validate(schema, "https://example.com")).toBe(false);
57
+ expect(reads).toBe(2);
58
+ reads = 0;
59
+ expect(schema.safeParse("https://example.com").success).toBe(false);
60
+ expect(reads).toBe(2);
61
+ }
62
+ } finally {
63
+ z.core.globalConfig.postProcessor = postProcessor;
64
+ }
65
+ });
66
+
67
+ test("the exported object parser still returns a URL", async () => {
68
+ const { parseURLObject } = await import("../schemas.js");
69
+ const result = parseURLObject("https://example.com", {});
70
+ expect(result).toBeInstanceOf(NativeURL);
71
+ if (typeof result === "number") expect.unreachable();
72
+ expect(result.hostname).toBe("example.com");
73
+ expect(parseURLObject("invalid", {})).toBe(2);
22
74
  });
23
75
 
24
- test("z.url() validates correctly without URL.canParse (compiled)", async () => {
25
- expect(URL.canParse).toBeUndefined();
76
+ test("falls back when URL.canParse is absent", async () => {
77
+ Reflect.deleteProperty(NativeURL, "canParse");
26
78
  const z = await import("../../index.js");
27
- const compiled = z.compile(z.url());
28
- expect(compiled.safeParse("https://example.com").success).toBe(true);
29
- expect(compiled.safeParse("not a url").success).toBe(false);
79
+ const cases = [
80
+ [z.url(), "https://example.com", "not a url"],
81
+ [z.ipv6(), "2001:db8::1", "not-an-ip"],
82
+ [z.cidrv6(), "2001:db8::/32", "not-an-ip/32"],
83
+ ] as const;
84
+
85
+ for (const [schema, valid, invalid] of cases) {
86
+ for (const subject of [schema, z.compile(schema)]) {
87
+ expect(z.validate(subject, valid)).toBe(true);
88
+ expect(z.validate(subject, invalid)).toBe(false);
89
+ expect(subject.safeParse(valid).success).toBe(true);
90
+ expect(subject.safeParse(invalid).success).toBe(false);
91
+ }
92
+ }
93
+ });
94
+
95
+ test("falls back when URL.parse is absent", async () => {
96
+ Reflect.deleteProperty(NativeURL, "parse");
97
+ const z = await import("../../index.js");
98
+ const cases = [
99
+ [z.url({ normalize: true }), "https://example.com", "not a url"],
100
+ [z.url({ hostname: /^example\.com$/ }), "https://example.com", "https://other.com"],
101
+ [z.httpUrl(), "https://example.com", "mailto:a@example.com"],
102
+ ] as const;
103
+
104
+ for (const [schema, valid, invalid] of cases) {
105
+ for (const subject of [schema, z.compile(schema)]) {
106
+ expect(z.validate(subject, valid)).toBe(true);
107
+ expect(z.validate(subject, invalid)).toBe(false);
108
+ expect(subject.safeParse(valid).success).toBe(true);
109
+ expect(subject.safeParse(invalid).success).toBe(false);
110
+ }
111
+ }
112
+ });
113
+
114
+ test("rejects without a global URL constructor", async () => {
115
+ const z = await import("../../index.js");
116
+ const cases = [
117
+ [z.url(), "https://example.com"],
118
+ [z.url({ normalize: true }), "https://example.com"],
119
+ [z.ipv6(), "2001:db8::1"],
120
+ [z.cidrv6(), "2001:db8::/32"],
121
+ ] as const;
122
+ const subjects = cases.flatMap(([schema, input]) => [
123
+ [schema, input] as const,
124
+ [z.compile(schema), input] as const,
125
+ ]);
126
+ Object.defineProperty(globalThis, "URL", { configurable: true, value: undefined });
127
+
128
+ for (const [schema, input] of subjects) {
129
+ expect(z.validate(schema, input)).toBe(false);
130
+ expect(schema.safeParse(input).success).toBe(false);
131
+ }
30
132
  });
31
133
  });
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 6,
4
- patch: 3 as number,
4
+ patch: 5 as number,
5
5
  } as const;
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "نَص على هيئة JSON",
47
47
  e164: "رقم هاتف بمعيار E.164",
48
48
  credit_card: "رقم بطاقة الائتمان",
49
+ currency_code: "رمز العملة",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "مدخل",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "JSON string",
47
47
  e164: "E.164 number",
48
48
  credit_card: "kredit kartı nömrəsi",
49
+ currency_code: "valyuta kodu",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "input",
@@ -109,6 +109,7 @@ const error: () => errors.$ZodErrorMap = () => {
109
109
  json_string: "JSON радок",
110
110
  e164: "нумар E.164",
111
111
  credit_card: "нумар крэдытнай карты",
112
+ currency_code: "код валюты",
112
113
  iban: "IBAN",
113
114
  jwt: "JWT",
114
115
  template_literal: "увод",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "JSON низ",
47
47
  e164: "E.164 номер",
48
48
  credit_card: "номер на кредитна карта",
49
+ currency_code: "код на валута",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "вход",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "JSON স্ট্রিং",
47
47
  e164: "E.164 নম্বর",
48
48
  credit_card: "ক্রেডিট কার্ড নম্বর",
49
+ currency_code: "মুদ্রা কোড",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "ইনপুট",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "cadena JSON",
47
47
  e164: "número E.164",
48
48
  credit_card: "número de targeta de crèdit",
49
+ currency_code: "codi de moneda",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "entrada",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "دەقی JSON",
47
47
  e164: "ژمارەی E.164",
48
48
  credit_card: "ژمارەی کارتی کرێدیت",
49
+ currency_code: "کۆدی دراو",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "تێکردە",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "řetězec ve formátu JSON",
47
47
  e164: "číslo E.164",
48
48
  credit_card: "číslo kreditní karty",
49
+ currency_code: "kód měny",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "vstup",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "JSON-streng",
47
47
  e164: "E.164-nummer",
48
48
  credit_card: "kreditkortnummer",
49
+ currency_code: "valutakode",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "input",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "JSON-String",
47
47
  e164: "E.164-Nummer",
48
48
  credit_card: "Kreditkartennummer",
49
+ currency_code: "Währungscode",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "Eingabe",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "συμβολοσειρά JSON",
47
47
  e164: "αριθμός E.164",
48
48
  credit_card: "αριθμός πιστωτικής κάρτας",
49
+ currency_code: "κωδικός νομίσματος",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "είσοδος",
@@ -45,6 +45,7 @@ const error: () => errors.$ZodErrorMap = () => {
45
45
  base64url: "base64url-encoded string",
46
46
  json_string: "JSON string",
47
47
  e164: "E.164 number",
48
+ currency_code: "currency code",
48
49
  credit_card: "credit card number",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "JSON-karaktraro",
47
47
  e164: "E.164-nombro",
48
48
  credit_card: "kreditkarta numero",
49
+ currency_code: "valuta kodo",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "enigo",
@@ -45,6 +45,7 @@ const error: () => errors.$ZodErrorMap = () => {
45
45
  json_string: "cadena JSON",
46
46
  e164: "número E.164",
47
47
  credit_card: "número de tarjeta de crédito",
48
+ currency_code: "código de moneda",
48
49
  iban: "IBAN",
49
50
  jwt: "JWT",
50
51
  template_literal: "entrada",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "JSON رشته",
47
47
  e164: "E.164 عدد",
48
48
  credit_card: "شماره کارت اعتباری",
49
+ currency_code: "کد ارز",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "ورودی",
@@ -50,6 +50,7 @@ const error: () => errors.$ZodErrorMap = () => {
50
50
  json_string: "JSON-merkkijono",
51
51
  e164: "E.164-luku",
52
52
  credit_card: "luottokortin numero",
53
+ currency_code: "valuuttakoodi",
53
54
  iban: "IBAN",
54
55
  jwt: "JWT",
55
56
  template_literal: "templaattimerkkijono",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "chaîne JSON",
47
47
  e164: "numéro E.164",
48
48
  credit_card: "numéro de carte de crédit",
49
+ currency_code: "code de devise",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "entrée",
@@ -45,6 +45,7 @@ const error: () => errors.$ZodErrorMap = () => {
45
45
  json_string: "chaîne de caractères JSON",
46
46
  e164: "numéro au format E.164",
47
47
  credit_card: "numéro de carte de crédit",
48
+ currency_code: "code de devise",
48
49
  iban: "IBAN",
49
50
  jwt: "JWT",
50
51
  template_literal: "entrée",
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "JSON સ્ટ્રિંગ",
47
47
  e164: "E.164 નંબર",
48
48
  credit_card: "ક્રેડિટ કાર્ડ નંબર",
49
+ currency_code: "ચલણ કોડ",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "ઇનપુટ",
@@ -88,6 +88,7 @@ const error: () => errors.$ZodErrorMap = () => {
88
88
  json_string: { label: "מחרוזת JSON", gender: "f" },
89
89
  e164: { label: "מספר E.164", gender: "m" },
90
90
  credit_card: { label: "מספר כרטיס אשראי", gender: "m" },
91
+ currency_code: { label: "קוד מטבע", gender: "m" },
91
92
  iban: { label: "IBAN", gender: "m" },
92
93
  jwt: { label: "JWT", gender: "m" },
93
94
  template_literal: { label: "קלט", gender: "m" },
@@ -46,6 +46,7 @@ const error: () => errors.$ZodErrorMap = () => {
46
46
  json_string: "JSON स्ट्रिंग",
47
47
  e164: "E.164 संख्या",
48
48
  credit_card: "क्रेडिट कार्ड संख्या",
49
+ currency_code: "मुद्रा कोड",
49
50
  iban: "IBAN",
50
51
  jwt: "JWT",
51
52
  template_literal: "इनपुट",