zod 4.1.12 → 4.1.13

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 (82) hide show
  1. package/package.json +1 -1
  2. package/src/v4/classic/checks.ts +1 -0
  3. package/src/v4/classic/schemas.ts +20 -0
  4. package/src/v4/classic/tests/continuability.test.ts +22 -0
  5. package/src/v4/classic/tests/describe-meta-checks.test.ts +27 -0
  6. package/src/v4/classic/tests/index.test.ts +55 -1
  7. package/src/v4/classic/tests/promise.test.ts +1 -1
  8. package/src/v4/classic/tests/readonly.test.ts +1 -1
  9. package/src/v4/classic/tests/record.test.ts +141 -9
  10. package/src/v4/classic/tests/registries.test.ts +5 -1
  11. package/src/v4/classic/tests/string.test.ts +72 -0
  12. package/src/v4/classic/tests/template-literal.test.ts +8 -0
  13. package/src/v4/classic/tests/to-json-schema.test.ts +97 -0
  14. package/src/v4/classic/tests/tuple.test.ts +18 -0
  15. package/src/v4/classic/tests/url.test.ts +13 -0
  16. package/src/v4/core/api.ts +45 -0
  17. package/src/v4/core/core.ts +22 -9
  18. package/src/v4/core/regexes.ts +6 -1
  19. package/src/v4/core/registries.ts +12 -1
  20. package/src/v4/core/schemas.ts +50 -33
  21. package/src/v4/core/tests/extend.test.ts +42 -1
  22. package/src/v4/core/tests/locales/he.test.ts +379 -0
  23. package/src/v4/core/tests/locales/nl.test.ts +46 -0
  24. package/src/v4/core/tests/record-constructor.test.ts +67 -0
  25. package/src/v4/core/tests/recursive-tuples.test.ts +45 -0
  26. package/src/v4/core/to-json-schema.ts +55 -91
  27. package/src/v4/core/util.ts +11 -0
  28. package/src/v4/core/versions.ts +1 -1
  29. package/src/v4/locales/en.ts +1 -0
  30. package/src/v4/locales/he.ts +202 -71
  31. package/src/v4/locales/nl.ts +10 -10
  32. package/src/v4/mini/iso.ts +4 -4
  33. package/src/v4/mini/schemas.ts +17 -0
  34. package/src/v4/mini/tests/functions.test.ts +0 -38
  35. package/src/v4/mini/tests/index.test.ts +24 -1
  36. package/src/v4/mini/tests/string.test.ts +32 -0
  37. package/v3/ZodError.d.cts +1 -1
  38. package/v3/ZodError.d.ts +1 -1
  39. package/v4/classic/checks.cjs +2 -1
  40. package/v4/classic/checks.d.cts +1 -1
  41. package/v4/classic/checks.d.ts +1 -1
  42. package/v4/classic/checks.js +1 -1
  43. package/v4/classic/schemas.cjs +15 -2
  44. package/v4/classic/schemas.d.cts +8 -0
  45. package/v4/classic/schemas.d.ts +8 -0
  46. package/v4/classic/schemas.js +12 -0
  47. package/v4/core/api.cjs +40 -0
  48. package/v4/core/api.d.cts +7 -0
  49. package/v4/core/api.d.ts +7 -0
  50. package/v4/core/api.js +36 -0
  51. package/v4/core/core.cjs +20 -11
  52. package/v4/core/core.js +20 -11
  53. package/v4/core/regexes.cjs +31 -2
  54. package/v4/core/regexes.d.cts +1 -0
  55. package/v4/core/regexes.d.ts +1 -0
  56. package/v4/core/regexes.js +5 -0
  57. package/v4/core/registries.cjs +3 -1
  58. package/v4/core/registries.js +3 -1
  59. package/v4/core/schemas.cjs +32 -33
  60. package/v4/core/schemas.d.cts +12 -2
  61. package/v4/core/schemas.d.ts +12 -2
  62. package/v4/core/schemas.js +30 -31
  63. package/v4/core/to-json-schema.cjs +55 -92
  64. package/v4/core/to-json-schema.js +55 -92
  65. package/v4/core/util.cjs +11 -0
  66. package/v4/core/util.d.cts +1 -0
  67. package/v4/core/util.d.ts +1 -0
  68. package/v4/core/util.js +10 -0
  69. package/v4/core/versions.cjs +1 -1
  70. package/v4/core/versions.js +1 -1
  71. package/v4/locales/en.cjs +1 -0
  72. package/v4/locales/en.js +1 -0
  73. package/v4/locales/he.cjs +177 -66
  74. package/v4/locales/he.js +177 -66
  75. package/v4/locales/nl.cjs +8 -8
  76. package/v4/locales/nl.js +8 -8
  77. package/v4/mini/iso.cjs +4 -4
  78. package/v4/mini/iso.js +4 -4
  79. package/v4/mini/schemas.cjs +13 -2
  80. package/v4/mini/schemas.d.cts +6 -0
  81. package/v4/mini/schemas.d.ts +6 -0
  82. package/v4/mini/schemas.js +10 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod",
3
- "version": "4.1.12",
3
+ "version": "4.1.13",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -27,5 +27,6 @@ export {
27
27
  _trim as trim,
28
28
  _toLowerCase as toLowerCase,
29
29
  _toUpperCase as toUpperCase,
30
+ _slugify as slugify,
30
31
  type $RefinementCtx as RefinementCtx,
31
32
  } from "../core/index.js";
@@ -252,6 +252,7 @@ export interface _ZodString<T extends core.$ZodStringInternals<unknown> = core.$
252
252
  normalize(form?: "NFC" | "NFD" | "NFKC" | "NFKD" | (string & {})): this;
253
253
  toLowerCase(): this;
254
254
  toUpperCase(): this;
255
+ slugify(): this;
255
256
  }
256
257
 
257
258
  /** @internal */
@@ -281,6 +282,7 @@ export const _ZodString: core.$constructor<_ZodString> = /*@__PURE__*/ core.$con
281
282
  inst.normalize = (...args) => inst.check(checks.normalize(...args));
282
283
  inst.toLowerCase = () => inst.check(checks.toLowerCase());
283
284
  inst.toUpperCase = () => inst.check(checks.toUpperCase());
285
+ inst.slugify = () => inst.check(checks.slugify());
284
286
  });
285
287
 
286
288
  export interface ZodString extends _ZodString<core.$ZodStringInternals<string>> {
@@ -614,6 +616,19 @@ export function ipv4(params?: string | core.$ZodIPv4Params): ZodIPv4 {
614
616
  return core._ipv4(ZodIPv4, params);
615
617
  }
616
618
 
619
+ // ZodMAC
620
+ export interface ZodMAC extends ZodStringFormat<"mac"> {
621
+ _zod: core.$ZodMACInternals;
622
+ }
623
+ export const ZodMAC: core.$constructor<ZodMAC> = /*@__PURE__*/ core.$constructor("ZodMAC", (inst, def) => {
624
+ // ZodStringFormat.init(inst, def);
625
+ core.$ZodMAC.init(inst, def);
626
+ ZodStringFormat.init(inst, def);
627
+ });
628
+ export function mac(params?: string | core.$ZodMACParams): ZodMAC {
629
+ return core._mac(ZodMAC, params);
630
+ }
631
+
617
632
  // ZodIPv6
618
633
  export interface ZodIPv6 extends ZodStringFormat<"ipv6"> {
619
634
  _zod: core.$ZodIPv6Internals;
@@ -792,6 +807,7 @@ export interface ZodNumber extends _ZodNumber<core.$ZodNumberInternals<number>>
792
807
 
793
808
  export const ZodNumber: core.$constructor<ZodNumber> = /*@__PURE__*/ core.$constructor("ZodNumber", (inst, def) => {
794
809
  core.$ZodNumber.init(inst, def);
810
+
795
811
  ZodType.init(inst, def);
796
812
 
797
813
  inst.gt = (value, params) => inst.check(checks.gt(value, params));
@@ -2128,6 +2144,10 @@ export function superRefine<T>(
2128
2144
  return core._superRefine(fn);
2129
2145
  }
2130
2146
 
2147
+ // Re-export describe and meta from core
2148
+ export const describe = core.describe;
2149
+ export const meta = core.meta;
2150
+
2131
2151
  type ZodInstanceOfParams = core.Params<
2132
2152
  ZodCustom,
2133
2153
  core.$ZodIssueCustom,
@@ -195,6 +195,28 @@ test("continuability", () => {
195
195
  },
196
196
  ]
197
197
  `);
198
+ expect(
199
+ z
200
+ .mac()
201
+ .refine(() => false)
202
+ .safeParse("invalid_value").error!.issues
203
+ ).toMatchInlineSnapshot(`
204
+ [
205
+ {
206
+ "code": "invalid_format",
207
+ "format": "mac",
208
+ "message": "Invalid MAC address",
209
+ "origin": "string",
210
+ "path": [],
211
+ "pattern": "/^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$/",
212
+ },
213
+ {
214
+ "code": "custom",
215
+ "message": "Invalid input",
216
+ "path": [],
217
+ },
218
+ ]
219
+ `);
198
220
  expect(
199
221
  z
200
222
  .emoji()
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as z from "../index.js";
3
+
4
+ describe("z.describe() check", () => {
5
+ it("registers description in globalRegistry", () => {
6
+ const schema = z.string().check(z.describe("A string"));
7
+ expect(z.globalRegistry.get(schema)?.description).toBe("A string");
8
+ });
9
+ });
10
+
11
+ describe("z.meta() check", () => {
12
+ it("registers metadata in globalRegistry", () => {
13
+ const schema = z.number().check(z.meta({ title: "Age", description: "User's age" }));
14
+ const meta = z.globalRegistry.get(schema);
15
+ expect(meta?.title).toBe("Age");
16
+ expect(meta?.description).toBe("User's age");
17
+ });
18
+ });
19
+
20
+ describe("combined usage", () => {
21
+ it("works with multiple checks", () => {
22
+ const schema = z.string().check(z.describe("Email address"), z.meta({ title: "Email" }));
23
+ const meta = z.globalRegistry.get(schema);
24
+ expect(meta?.description).toBe("Email address");
25
+ expect(meta?.title).toBe("Email");
26
+ });
27
+ });
@@ -302,6 +302,29 @@ test("z.record", () => {
302
302
  const d = z.record(z.enum(["a", "b"]).or(z.never()), z.string());
303
303
  type d = z.output<typeof d>;
304
304
  expectTypeOf<d>().toEqualTypeOf<Record<"a" | "b", string>>();
305
+
306
+ // literal union keys
307
+ const e = z.record(z.union([z.literal("a"), z.literal(0)]), z.string());
308
+ type e = z.output<typeof e>;
309
+ expectTypeOf<e>().toEqualTypeOf<Record<"a" | 0, string>>();
310
+ expect(z.parse(e, { a: "hello", 0: "world" })).toEqual({
311
+ a: "hello",
312
+ 0: "world",
313
+ });
314
+
315
+ // TypeScript enum keys
316
+ enum Enum {
317
+ A = 0,
318
+ B = "hi",
319
+ }
320
+
321
+ const f = z.record(z.enum(Enum), z.string());
322
+ type f = z.output<typeof f>;
323
+ expectTypeOf<f>().toEqualTypeOf<Record<Enum, string>>();
324
+ expect(z.parse(f, { [Enum.A]: "hello", [Enum.B]: "world" })).toEqual({
325
+ [Enum.A]: "hello",
326
+ [Enum.B]: "world",
327
+ });
305
328
  });
306
329
 
307
330
  test("z.map", () => {
@@ -749,7 +772,7 @@ test("z.json", () => {
749
772
  test("z.promise", async () => {
750
773
  const a = z.promise(z.string());
751
774
  type a = z.output<typeof a>;
752
- expectTypeOf<a>().toEqualTypeOf<string>();
775
+ expectTypeOf<a>().toEqualTypeOf<Promise<string>>();
753
776
 
754
777
  expect(await z.safeParseAsync(a, Promise.resolve("hello"))).toMatchObject({
755
778
  success: true,
@@ -786,6 +809,37 @@ test("isPlainObject", () => {
786
809
  expect(z.core.util.isPlainObject("string")).toEqual(false);
787
810
  expect(z.core.util.isPlainObject(123)).toEqual(false);
788
811
  expect(z.core.util.isPlainObject(Symbol())).toEqual(false);
812
+ expect(z.core.util.isPlainObject({ constructor: "string" })).toEqual(true);
813
+ expect(z.core.util.isPlainObject({ constructor: 123 })).toEqual(true);
814
+ expect(z.core.util.isPlainObject({ constructor: null })).toEqual(true);
815
+ expect(z.core.util.isPlainObject({ constructor: undefined })).toEqual(true);
816
+ expect(z.core.util.isPlainObject({ constructor: true })).toEqual(true);
817
+ expect(z.core.util.isPlainObject({ constructor: {} })).toEqual(true);
818
+ expect(z.core.util.isPlainObject({ constructor: [] })).toEqual(true);
819
+ });
820
+
821
+ test("shallowClone with constructor field", () => {
822
+ const objWithConstructor = { constructor: "string", key: "value" };
823
+ const cloned = z.core.util.shallowClone(objWithConstructor);
824
+
825
+ expect(cloned).toEqual(objWithConstructor);
826
+ expect(cloned).not.toBe(objWithConstructor);
827
+ expect(cloned.constructor).toBe("string");
828
+ expect(cloned.key).toBe("value");
829
+
830
+ const testCases = [
831
+ { constructor: 123, data: "test" },
832
+ { constructor: null, data: "test" },
833
+ { constructor: true, data: "test" },
834
+ { constructor: {}, data: "test" },
835
+ { constructor: [], data: "test" },
836
+ ];
837
+
838
+ for (const testCase of testCases) {
839
+ const clonedCase = z.core.util.shallowClone(testCase);
840
+ expect(clonedCase).toEqual(testCase);
841
+ expect(clonedCase).not.toBe(testCase);
842
+ }
789
843
  });
790
844
 
791
845
  test("def typing", () => {
@@ -10,7 +10,7 @@ const promSchema = z.promise(
10
10
 
11
11
  test("promise inference", () => {
12
12
  type promSchemaType = z.infer<typeof promSchema>;
13
- expectTypeOf<promSchemaType>().toEqualTypeOf<{ name: string; age: number }>();
13
+ expectTypeOf<promSchemaType>().toEqualTypeOf<Promise<{ name: string; age: number }>>();
14
14
  });
15
15
 
16
16
  test("promise parsing success", async () => {
@@ -47,7 +47,7 @@ test("flat inference", () => {
47
47
  expectTypeOf<typeof readonlyNumberRecord._output>().toEqualTypeOf<Readonly<Record<string, number>>>();
48
48
  expectTypeOf<typeof readonlyObject._output>().toEqualTypeOf<{ readonly a: string; readonly 1: number }>();
49
49
  expectTypeOf<typeof readonlyEnum._output>().toEqualTypeOf<Readonly<testEnum>>();
50
- expectTypeOf<typeof readonlyPromise._output>().toEqualTypeOf<string>();
50
+ expectTypeOf<typeof readonlyPromise._output>().toEqualTypeOf<Promise<string>>();
51
51
  });
52
52
 
53
53
  // test("deep inference", () => {
@@ -8,16 +8,28 @@ test("type inference", () => {
8
8
  const recordWithEnumKeys = z.record(z.enum(["Tuna", "Salmon"]), z.string());
9
9
  type recordWithEnumKeys = z.infer<typeof recordWithEnumKeys>;
10
10
 
11
- const recordWithLiteralKey = z.record(z.literal(["Tuna", "Salmon"]), z.string());
11
+ const recordWithLiteralKey = z.record(z.literal(["Tuna", "Salmon", 21]), z.string());
12
12
  type recordWithLiteralKey = z.infer<typeof recordWithLiteralKey>;
13
13
 
14
- const recordWithLiteralUnionKeys = z.record(z.union([z.literal("Tuna"), z.literal("Salmon")]), z.string());
14
+ const recordWithLiteralUnionKeys = z.record(
15
+ z.union([z.literal("Tuna"), z.literal("Salmon"), z.literal(21)]),
16
+ z.string()
17
+ );
15
18
  type recordWithLiteralUnionKeys = z.infer<typeof recordWithLiteralUnionKeys>;
16
19
 
20
+ enum Enum {
21
+ Tuna = 0,
22
+ Salmon = "Shark",
23
+ }
24
+
25
+ const recordWithTypescriptEnum = z.record(z.enum(Enum), z.string());
26
+ type recordWithTypescriptEnum = z.infer<typeof recordWithTypescriptEnum>;
27
+
17
28
  expectTypeOf<booleanRecord>().toEqualTypeOf<Record<string, boolean>>();
18
29
  expectTypeOf<recordWithEnumKeys>().toEqualTypeOf<Record<"Tuna" | "Salmon", string>>();
19
- expectTypeOf<recordWithLiteralKey>().toEqualTypeOf<Record<"Tuna" | "Salmon", string>>();
20
- expectTypeOf<recordWithLiteralUnionKeys>().toEqualTypeOf<Record<"Tuna" | "Salmon", string>>();
30
+ expectTypeOf<recordWithLiteralKey>().toEqualTypeOf<Record<"Tuna" | "Salmon" | 21, string>>();
31
+ expectTypeOf<recordWithLiteralUnionKeys>().toEqualTypeOf<Record<"Tuna" | "Salmon" | 21, string>>();
32
+ expectTypeOf<recordWithTypescriptEnum>().toEqualTypeOf<Record<Enum, string>>();
21
33
  });
22
34
 
23
35
  test("enum exhaustiveness", () => {
@@ -64,14 +76,76 @@ test("enum exhaustiveness", () => {
64
76
  `);
65
77
  });
66
78
 
79
+ test("typescript enum exhaustiveness", () => {
80
+ enum BigFish {
81
+ Tuna = 0,
82
+ Salmon = "Shark",
83
+ }
84
+
85
+ const schema = z.record(z.enum(BigFish), z.string());
86
+ const value = {
87
+ [BigFish.Tuna]: "asdf",
88
+ [BigFish.Salmon]: "asdf",
89
+ };
90
+
91
+ expect(schema.parse(value)).toEqual(value);
92
+
93
+ expect(schema.safeParse({ [BigFish.Tuna]: "asdf", [BigFish.Salmon]: "asdf", Trout: "asdf" })).toMatchInlineSnapshot(`
94
+ {
95
+ "error": [ZodError: [
96
+ {
97
+ "code": "unrecognized_keys",
98
+ "keys": [
99
+ "Trout"
100
+ ],
101
+ "path": [],
102
+ "message": "Unrecognized key: \\"Trout\\""
103
+ }
104
+ ]],
105
+ "success": false,
106
+ }
107
+ `);
108
+ expect(schema.safeParse({ [BigFish.Tuna]: "asdf" })).toMatchInlineSnapshot(`
109
+ {
110
+ "error": [ZodError: [
111
+ {
112
+ "expected": "string",
113
+ "code": "invalid_type",
114
+ "path": [
115
+ "Shark"
116
+ ],
117
+ "message": "Invalid input: expected string, received undefined"
118
+ }
119
+ ]],
120
+ "success": false,
121
+ }
122
+ `);
123
+ expect(schema.safeParse({ [BigFish.Salmon]: "asdf" })).toMatchInlineSnapshot(`
124
+ {
125
+ "error": [ZodError: [
126
+ {
127
+ "expected": "string",
128
+ "code": "invalid_type",
129
+ "path": [
130
+ 0
131
+ ],
132
+ "message": "Invalid input: expected string, received undefined"
133
+ }
134
+ ]],
135
+ "success": false,
136
+ }
137
+ `);
138
+ });
139
+
67
140
  test("literal exhaustiveness", () => {
68
- const schema = z.record(z.literal(["Tuna", "Salmon"]), z.string());
141
+ const schema = z.record(z.literal(["Tuna", "Salmon", 21]), z.string());
69
142
  schema.parse({
70
143
  Tuna: "asdf",
71
144
  Salmon: "asdf",
145
+ 21: "asdf",
72
146
  });
73
147
 
74
- expect(schema.safeParse({ Tuna: "asdf", Salmon: "asdf", Trout: "asdf" })).toMatchInlineSnapshot(`
148
+ expect(schema.safeParse({ Tuna: "asdf", Salmon: "asdf", 21: "asdf", Trout: "asdf" })).toMatchInlineSnapshot(`
75
149
  {
76
150
  "error": [ZodError: [
77
151
  {
@@ -96,6 +170,14 @@ test("literal exhaustiveness", () => {
96
170
  "Salmon"
97
171
  ],
98
172
  "message": "Invalid input: expected string, received undefined"
173
+ },
174
+ {
175
+ "expected": "string",
176
+ "code": "invalid_type",
177
+ "path": [
178
+ 21
179
+ ],
180
+ "message": "Invalid input: expected string, received undefined"
99
181
  }
100
182
  ]],
101
183
  "success": false,
@@ -143,13 +225,14 @@ test("pipe exhaustiveness", () => {
143
225
  });
144
226
 
145
227
  test("union exhaustiveness", () => {
146
- const schema = z.record(z.union([z.literal("Tuna"), z.literal("Salmon")]), z.string());
147
- expect(schema.parse({ Tuna: "asdf", Salmon: "asdf" })).toEqual({
228
+ const schema = z.record(z.union([z.literal("Tuna"), z.literal("Salmon"), z.literal(21)]), z.string());
229
+ expect(schema.parse({ Tuna: "asdf", Salmon: "asdf", 21: "asdf" })).toEqual({
148
230
  Tuna: "asdf",
149
231
  Salmon: "asdf",
232
+ 21: "asdf",
150
233
  });
151
234
 
152
- expect(schema.safeParse({ Tuna: "asdf", Salmon: "asdf", Trout: "asdf" })).toMatchInlineSnapshot(`
235
+ expect(schema.safeParse({ Tuna: "asdf", Salmon: "asdf", 21: "asdf", Trout: "asdf" })).toMatchInlineSnapshot(`
153
236
  {
154
237
  "error": [ZodError: [
155
238
  {
@@ -174,6 +257,14 @@ test("union exhaustiveness", () => {
174
257
  "Salmon"
175
258
  ],
176
259
  "message": "Invalid input: expected string, received undefined"
260
+ },
261
+ {
262
+ "expected": "string",
263
+ "code": "invalid_type",
264
+ "path": [
265
+ 21
266
+ ],
267
+ "message": "Invalid input: expected string, received undefined"
177
268
  }
178
269
  ]],
179
270
  "success": false,
@@ -354,3 +445,44 @@ test("partial record", () => {
354
445
 
355
446
  expect(Person.def.keyType._zod.def.type).toEqual("enum");
356
447
  });
448
+
449
+ test("partialRecord with z.literal([key, ...])", () => {
450
+ const Keys = z.literal(["id", "name", "email"]);
451
+ const schema = z.partialRecord(Keys, z.string());
452
+ type Schema = z.infer<typeof schema>;
453
+ expectTypeOf<Schema>().toEqualTypeOf<Partial<Record<"id" | "name" | "email", string>>>();
454
+
455
+ // Should parse valid partials
456
+ expect(schema.parse({})).toEqual({});
457
+ expect(schema.parse({ id: "1" })).toEqual({ id: "1" });
458
+ expect(schema.parse({ name: "n", email: "e@example.com" })).toEqual({ name: "n", email: "e@example.com" });
459
+
460
+ // Should fail with unrecognized key, error checked via inline snapshot
461
+ expect(schema.safeParse({ foo: "bar" })).toMatchInlineSnapshot(`
462
+ {
463
+ "error": [ZodError: [
464
+ {
465
+ "code": "invalid_key",
466
+ "origin": "record",
467
+ "issues": [
468
+ {
469
+ "code": "invalid_value",
470
+ "values": [
471
+ "id",
472
+ "name",
473
+ "email"
474
+ ],
475
+ "path": [],
476
+ "message": "Invalid option: expected one of \\"id\\"|\\"name\\"|\\"email\\""
477
+ }
478
+ ],
479
+ "path": [
480
+ "foo"
481
+ ],
482
+ "message": "Invalid key in record"
483
+ }
484
+ ]],
485
+ "success": false,
486
+ }
487
+ `);
488
+ });
@@ -19,6 +19,10 @@ test("globalRegistry", () => {
19
19
  expect(z.globalRegistry.has(a)).toEqual(false);
20
20
  });
21
21
 
22
+ test("globalRegistry is singleton and attached to globalThis", () => {
23
+ expect(z.globalRegistry).toBe((globalThis as any).__zod_globalRegistry);
24
+ });
25
+
22
26
  test("z.registry", () => {
23
27
  const fieldRegistry = z.registry<{ name: string; description: string }>();
24
28
 
@@ -164,7 +168,7 @@ test("loose examples", () => {
164
168
  });
165
169
  });
166
170
 
167
- test("function meta witout replacement", () => {
171
+ test("function meta without replacement", () => {
168
172
  const myReg = z.registry<{
169
173
  defaulter: (arg: string, test: boolean) => number;
170
174
  }>();
@@ -775,6 +775,8 @@ test("format", () => {
775
775
  expect(z.string().date().format).toEqual("date");
776
776
  expect(z.string().time().format).toEqual("time");
777
777
  expect(z.string().duration().format).toEqual("duration");
778
+
779
+ expect(z.mac().format).toEqual("mac");
778
780
  });
779
781
 
780
782
  test("min max getters", () => {
@@ -801,6 +803,24 @@ test("lowerCase", () => {
801
803
  expect(z.string().toUpperCase().parse("asdf")).toEqual("ASDF");
802
804
  });
803
805
 
806
+ test("slugify", () => {
807
+ expect(z.string().slugify().parse("Hello World")).toEqual("hello-world");
808
+ expect(z.string().slugify().parse(" Hello World ")).toEqual("hello-world");
809
+ expect(z.string().slugify().parse("Hello@World#123")).toEqual("helloworld123");
810
+ expect(z.string().slugify().parse("Hello-World")).toEqual("hello-world");
811
+ expect(z.string().slugify().parse("Hello_World")).toEqual("hello-world");
812
+ expect(z.string().slugify().parse("---Hello---World---")).toEqual("hello-world");
813
+ expect(z.string().slugify().parse("Hello World")).toEqual("hello-world");
814
+ expect(z.string().slugify().parse("Hello!@#$%^&*()World")).toEqual("helloworld");
815
+
816
+ // can be used with check
817
+ expect(z.string().check(z.slugify()).parse("Hello World")).toEqual("hello-world");
818
+
819
+ // can be chained with other methods
820
+ expect(z.string().slugify().min(5).parse("Hello World")).toEqual("hello-world");
821
+ expect(() => z.string().slugify().min(20).parse("Hello World")).toThrow();
822
+ });
823
+
804
824
  // test("IP validation", () => {
805
825
  // const ipSchema = z.string().ip();
806
826
 
@@ -885,6 +905,58 @@ test("IPv6 validation", () => {
885
905
  expect(() => ipv6.parse("254.164.77.1")).toThrow();
886
906
  });
887
907
 
908
+ test("MAC validation", () => {
909
+ const mac = z.mac();
910
+
911
+ // Valid MAC addresses
912
+ expect(mac.safeParse("00:1A:2B:3C:4D:5E").success).toBe(true);
913
+ expect(mac.safeParse("FF:FF:FF:FF:FF:FF").success).toBe(true);
914
+ expect(mac.safeParse("00:11:22:33:44:55").success).toBe(true);
915
+ expect(mac.safeParse("A1:B2:C3:D4:E5:F6").success).toBe(true);
916
+ expect(mac.safeParse("10:20:30:40:50:60").success).toBe(true);
917
+ expect(mac.safeParse("0a:1b:2c:3d:4e:5f").success).toBe(true);
918
+ expect(mac.safeParse("12:34:56:78:9A:BC").success).toBe(true);
919
+
920
+ // Invalid MAC addresses
921
+ expect(mac.safeParse("00:1A-2B:3C-4D:5E").success).toBe(false);
922
+ expect(mac.safeParse("00:1A:2B:3C:4D").success).toBe(false);
923
+ expect(mac.safeParse("00:1A:2B:3C:4D").success).toBe(false);
924
+ expect(mac.safeParse("00-1A-2B-3C-4D").success).toBe(false);
925
+ expect(mac.safeParse("01-23-45-67-89-AB").success).toBe(false); // Dash delimiter not accepted by default
926
+ expect(mac.safeParse("AA-BB-CC-DD-EE-FF").success).toBe(false); // Dash delimiter not accepted by default
927
+ expect(mac.safeParse("DE-AD-BE-EF-00-01").success).toBe(false); // Dash delimiter not accepted by default
928
+ expect(mac.safeParse("98-76-54-32-10-FF").success).toBe(false); // Dash delimiter not accepted by default
929
+ expect(mac.safeParse("00:1A:2B:3C:4D:GZ").success).toBe(false);
930
+ expect(mac.safeParse("00:1A:2B:3C:4D:5E:GG").success).toBe(false);
931
+ expect(mac.safeParse("123:45:67:89:AB:CD").success).toBe(false);
932
+ expect(mac.safeParse("00--1A:2B:3C:4D:5E").success).toBe(false);
933
+ expect(mac.safeParse("00:1A::2B:3C:4D:5E").success).toBe(false);
934
+ expect(mac.safeParse("00:1A:2B:3C:3C:2B:1A:00").success).toBe(false); // Disallow EUI-64
935
+ expect(mac.safeParse("00:1a:2B:3c:4D:5e").success).toBe(false); // Disallow mixed-case
936
+
937
+ // MAC formats that are nonstandard but occassionally referenced, ex. https://www.postgresql.org/docs/17/datatype-net-types.html#DATATYPE-MACADDR
938
+ expect(mac.safeParse("00:1A:2B:3C:4D:5E:FF").success).toBe(false);
939
+ expect(mac.safeParse("001A2B:3C4D5E").success).toBe(false);
940
+ expect(mac.safeParse("001A:2B3C:4D5E").success).toBe(false);
941
+ expect(mac.safeParse("001A.2B3C.4D5E").success).toBe(false);
942
+ expect(mac.safeParse("001A2B3C4D5E").success).toBe(false);
943
+ expect(mac.safeParse("00.1A.2B.3C.4D.5E").success).toBe(false);
944
+ });
945
+
946
+ test("MAC validation with custom delimiter", () => {
947
+ const colonMac = z.mac({ delimiter: ":" });
948
+ expect(colonMac.safeParse("00:1A:2B:3C:4D:5E").success).toBe(true);
949
+ expect(colonMac.safeParse("00-1A-2B-3C-4D-5E").success).toBe(false);
950
+
951
+ const dashMac = z.mac({ delimiter: "-" });
952
+ expect(dashMac.safeParse("00-1A-2B-3C-4D-5E").success).toBe(true);
953
+ expect(dashMac.safeParse("00:1A:2B:3C:4D:5E").success).toBe(false);
954
+
955
+ const colonOnlyMac = z.mac({ delimiter: ":" });
956
+ expect(colonOnlyMac.safeParse("00:1A:2B:3C:4D:5E").success).toBe(true);
957
+ expect(colonOnlyMac.safeParse("00-1A-2B-3C-4D-5E").success).toBe(false);
958
+ });
959
+
888
960
  test("CIDR v4 validation", () => {
889
961
  const cidrV4 = z.string().cidrv4();
890
962
 
@@ -43,6 +43,7 @@ const email = z.templateLiteral(["", z.string().email()]);
43
43
  // const ip = z.templateLiteral(["", z.string().ip()]);
44
44
  const ipv4 = z.templateLiteral(["", z.string().ipv4()]);
45
45
  const ipv6 = z.templateLiteral(["", z.string().ipv6()]);
46
+ const mac = z.templateLiteral(["", z.mac()]);
46
47
  const ulid = z.templateLiteral(["", z.string().ulid()]);
47
48
  const uuid = z.templateLiteral(["", z.string().uuid()]);
48
49
  const stringAToZ = z.templateLiteral(["", z.string().regex(/^[a-z]+$/)]);
@@ -137,6 +138,7 @@ test("template literal type inference", () => {
137
138
  // expectTypeOf<z.infer<typeof ip>>().toEqualTypeOf<string>();
138
139
  expectTypeOf<z.infer<typeof ipv4>>().toEqualTypeOf<string>();
139
140
  expectTypeOf<z.infer<typeof ipv6>>().toEqualTypeOf<string>();
141
+ expectTypeOf<z.infer<typeof mac>>().toEqualTypeOf<string>();
140
142
  expectTypeOf<z.infer<typeof ulid>>().toEqualTypeOf<string>();
141
143
  expectTypeOf<z.infer<typeof uuid>>().toEqualTypeOf<string>();
142
144
  expectTypeOf<z.infer<typeof stringAToZ>>().toEqualTypeOf<string>();
@@ -361,6 +363,7 @@ test("template literal parsing - success - basic cases", () => {
361
363
  // ip.parse("c359:f57c:21e5:39eb:1187:e501:f936:b452");
362
364
  ipv4.parse("213.174.246.205");
363
365
  ipv6.parse("c359:f57c:21e5:39eb:1187:e501:f936:b452");
366
+ mac.parse("00:1A:2B:3C:4D:5E");
364
367
  ulid.parse("01GW3D2QZJBYB6P1Z1AE997VPW");
365
368
  uuid.parse("808989fd-3a6e-4af2-b607-737323a176f6");
366
369
  stringAToZ.parse("asudgaskhdgashd");
@@ -497,6 +500,8 @@ test("template literal parsing - failure - basic cases", () => {
497
500
  expect(() => ipv4.parse("c359:f57c:21e5:39eb:1187:e501:f936:b452")).toThrow();
498
501
  expect(() => ipv6.parse("c359:f57c:21e5:39eb:1187:e501:f936:b4521")).toThrow();
499
502
  expect(() => ipv6.parse("213.174.246.205")).toThrow();
503
+ expect(() => mac.parse("00:1A:2B:3C:4D:5E:6A:7B")).toThrow();
504
+ expect(() => mac.parse("00:1A:2B:3C")).toThrow();
500
505
  expect(() => ulid.parse("01GW3D2QZJBYB6P1Z1AE997VPW!")).toThrow();
501
506
  expect(() => uuid.parse("808989fd-3a6e-4af2-b607-737323a176f6Z")).toThrow();
502
507
  expect(() => uuid.parse("Z808989fd-3a6e-4af2-b607-737323a176f6")).toThrow();
@@ -568,6 +573,9 @@ test("regexes", () => {
568
573
  expect(ipv6._zod.pattern.source).toMatchInlineSnapshot(
569
574
  `"^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$"`
570
575
  );
576
+ expect(mac._zod.pattern.source).toMatchInlineSnapshot(
577
+ `"^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$"`
578
+ );
571
579
  expect(ulid._zod.pattern.source).toMatchInlineSnapshot(`"^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$"`);
572
580
  expect(uuid._zod.pattern.source).toMatchInlineSnapshot(
573
581
  `"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"`