zod 4.1.0-canary.20250821T014902 → 4.1.0-canary.20250823T064644

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 (106) hide show
  1. package/package.json +1 -1
  2. package/src/v3/tests/object.test.ts +2 -2
  3. package/src/v4/classic/external.ts +0 -1
  4. package/src/v4/classic/parse.ts +49 -0
  5. package/src/v4/classic/schemas.ts +145 -7
  6. package/src/v4/classic/tests/array.test.ts +6 -6
  7. package/src/v4/classic/tests/catch.test.ts +25 -0
  8. package/src/v4/classic/tests/codec-examples.test.ts +538 -0
  9. package/src/v4/classic/tests/codec.test.ts +532 -0
  10. package/src/v4/classic/tests/continuability.test.ts +1 -1
  11. package/src/v4/classic/tests/default.test.ts +32 -0
  12. package/src/v4/classic/tests/firstparty.test.ts +4 -0
  13. package/src/v4/classic/tests/function.test.ts +31 -31
  14. package/src/v4/classic/tests/hash.test.ts +68 -0
  15. package/src/v4/classic/tests/nonoptional.test.ts +15 -0
  16. package/src/v4/classic/tests/object.test.ts +33 -2
  17. package/src/v4/classic/tests/pipe.test.ts +25 -5
  18. package/src/v4/classic/tests/prefault.test.ts +25 -0
  19. package/src/v4/classic/tests/preprocess.test.ts +1 -6
  20. package/src/v4/classic/tests/refine.test.ts +78 -5
  21. package/src/v4/classic/tests/set.test.ts +1 -1
  22. package/src/v4/classic/tests/string-formats.test.ts +16 -0
  23. package/src/v4/classic/tests/string.test.ts +82 -1
  24. package/src/v4/classic/tests/stringbool.test.ts +40 -0
  25. package/src/v4/classic/tests/template-literal.test.ts +1 -1
  26. package/src/v4/classic/tests/to-json-schema.test.ts +21 -2
  27. package/src/v4/classic/tests/transform.test.ts +7 -0
  28. package/src/v4/classic/tests/union.test.ts +1 -1
  29. package/src/v4/core/api.ts +25 -35
  30. package/src/v4/core/core.ts +7 -26
  31. package/src/v4/core/index.ts +0 -1
  32. package/src/v4/core/json-schema.ts +1 -0
  33. package/src/v4/core/parse.ts +101 -0
  34. package/src/v4/core/regexes.ts +40 -1
  35. package/src/v4/core/schemas.ts +521 -129
  36. package/src/v4/core/to-json-schema.ts +43 -8
  37. package/src/v4/core/util.ts +73 -0
  38. package/src/v4/mini/external.ts +0 -1
  39. package/src/v4/mini/parse.ts +14 -1
  40. package/src/v4/mini/schemas.ts +153 -12
  41. package/src/v4/mini/tests/codec.test.ts +499 -0
  42. package/src/v4/mini/tests/object.test.ts +9 -0
  43. package/src/v4/mini/tests/string.test.ts +16 -0
  44. package/v4/classic/external.cjs +1 -2
  45. package/v4/classic/external.d.cts +1 -1
  46. package/v4/classic/external.d.ts +1 -1
  47. package/v4/classic/external.js +1 -1
  48. package/v4/classic/parse.cjs +10 -1
  49. package/v4/classic/parse.d.cts +8 -0
  50. package/v4/classic/parse.d.ts +8 -0
  51. package/v4/classic/parse.js +9 -0
  52. package/v4/classic/schemas.cjs +59 -4
  53. package/v4/classic/schemas.d.cts +48 -2
  54. package/v4/classic/schemas.d.ts +48 -2
  55. package/v4/classic/schemas.js +51 -3
  56. package/v4/core/api.cjs +19 -24
  57. package/v4/core/api.d.cts +3 -4
  58. package/v4/core/api.d.ts +3 -4
  59. package/v4/core/api.js +19 -24
  60. package/v4/core/core.cjs +8 -1
  61. package/v4/core/core.d.cts +3 -0
  62. package/v4/core/core.d.ts +3 -0
  63. package/v4/core/core.js +6 -0
  64. package/v4/core/index.cjs +0 -1
  65. package/v4/core/index.d.cts +0 -1
  66. package/v4/core/index.d.ts +0 -1
  67. package/v4/core/index.js +0 -1
  68. package/v4/core/json-schema.d.cts +1 -0
  69. package/v4/core/json-schema.d.ts +1 -0
  70. package/v4/core/parse.cjs +45 -1
  71. package/v4/core/parse.d.cts +24 -0
  72. package/v4/core/parse.d.ts +24 -0
  73. package/v4/core/parse.js +36 -0
  74. package/v4/core/regexes.cjs +34 -2
  75. package/v4/core/regexes.d.cts +16 -0
  76. package/v4/core/regexes.d.ts +16 -0
  77. package/v4/core/regexes.js +32 -1
  78. package/v4/core/schemas.cjs +309 -77
  79. package/v4/core/schemas.d.cts +61 -3
  80. package/v4/core/schemas.d.ts +61 -3
  81. package/v4/core/schemas.js +308 -76
  82. package/v4/core/to-json-schema.cjs +42 -5
  83. package/v4/core/to-json-schema.d.cts +4 -3
  84. package/v4/core/to-json-schema.d.ts +4 -3
  85. package/v4/core/to-json-schema.js +42 -5
  86. package/v4/core/util.cjs +69 -0
  87. package/v4/core/util.d.cts +10 -0
  88. package/v4/core/util.d.ts +10 -0
  89. package/v4/core/util.js +62 -0
  90. package/v4/mini/external.cjs +1 -2
  91. package/v4/mini/external.d.cts +1 -1
  92. package/v4/mini/external.d.ts +1 -1
  93. package/v4/mini/external.js +1 -1
  94. package/v4/mini/parse.cjs +9 -1
  95. package/v4/mini/parse.d.cts +1 -1
  96. package/v4/mini/parse.d.ts +1 -1
  97. package/v4/mini/parse.js +1 -1
  98. package/v4/mini/schemas.cjs +58 -3
  99. package/v4/mini/schemas.d.cts +49 -1
  100. package/v4/mini/schemas.d.ts +49 -1
  101. package/v4/mini/schemas.js +49 -2
  102. package/src/v4/core/function.ts +0 -176
  103. package/v4/core/function.cjs +0 -102
  104. package/v4/core/function.d.cts +0 -52
  105. package/v4/core/function.d.ts +0 -52
  106. package/v4/core/function.js +0 -75
@@ -0,0 +1,68 @@
1
+ import { expect, expectTypeOf, test } from "vitest";
2
+ import { type ZodCustomStringFormat, hash } from "zod"; // adjust path as needed
3
+
4
+ test("hash() API — types and runtime across all alg/enc combinations", async () => {
5
+ const { createHash } = await import("node:crypto");
6
+
7
+ type Alg = "md5" | "sha1" | "sha256" | "sha384" | "sha512";
8
+ // type Enc = "hex" | "base64" | "base64url";
9
+
10
+ const toB64Url = (b64: string) => b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
11
+
12
+ const makeDigests = (alg: Alg, input: string) => {
13
+ const buf = createHash(alg).update(input).digest();
14
+ const hex = buf.toString("hex");
15
+ const base64 = buf.toString("base64");
16
+ const base64url = toB64Url(base64);
17
+ return { hex, base64, base64url };
18
+ };
19
+
20
+ const algs: ReadonlyArray<Alg> = ["md5", "sha1", "sha256", "sha384", "sha512"];
21
+ const input = "zodasklfjaasdf";
22
+
23
+ // --- Type-level checks (ensure the literal format string is encoded in the return type)
24
+ expectTypeOf(hash("md5")).toEqualTypeOf<ZodCustomStringFormat<"md5_hex">>();
25
+ expectTypeOf(hash("sha1")).toEqualTypeOf<ZodCustomStringFormat<"sha1_hex">>();
26
+ expectTypeOf(hash("sha256", { enc: "base64" as const })).toEqualTypeOf<ZodCustomStringFormat<"sha256_base64">>();
27
+ expectTypeOf(hash("sha384", { enc: "base64url" as const })).toEqualTypeOf<
28
+ ZodCustomStringFormat<"sha384_base64url">
29
+ >();
30
+
31
+ // Test generic format types are correctly inferred and Enc defaults to "hex"
32
+ expectTypeOf(hash("sha256")).toEqualTypeOf<ZodCustomStringFormat<"sha256_hex">>();
33
+
34
+ // --- Runtime matrix (success + a few sharp-edged failures per combo)
35
+ for (const alg of algs) {
36
+ const { hex, base64, base64url } = makeDigests(alg, input);
37
+
38
+ // Success cases
39
+ expect(hash(alg).parse(hex)).toBe(hex); // default enc=hex
40
+ expect(hash(alg, { enc: "hex" }).parse(hex)).toBe(hex);
41
+ expect(hash(alg, { enc: "base64" }).parse(base64)).toBe(base64);
42
+ expect(hash(alg, { enc: "base64url" }).parse(base64url)).toBe(base64url);
43
+
44
+ // Failure cases (wrong encoding to schema)
45
+ expect(() => hash(alg, { enc: "hex" }).parse(base64)).toThrow();
46
+ expect(() => hash(alg, { enc: "base64" }).parse(hex)).toThrow();
47
+ expect(() => hash(alg, { enc: "base64url" }).parse(base64)).toThrow();
48
+
49
+ // Encoding-specific failures
50
+ // hex: uppercase allowed, wrong length should fail
51
+ hash(alg, { enc: "hex" }).parse(hex.toUpperCase());
52
+ expect(() => hash(alg, { enc: "hex" }).parse(hex.slice(0, -1))).toThrow();
53
+
54
+ // base64: missing required padding should fail (only for algorithms that require padding)
55
+ if (base64.includes("=")) {
56
+ const base64NoPad = base64.replace(/=+$/g, "");
57
+ expect(() => hash(alg, { enc: "base64" }).parse(base64NoPad)).toThrow();
58
+ }
59
+
60
+ // base64url: adding padding or using invalid characters should fail
61
+ expect(() => hash(alg, { enc: "base64url" }).parse(base64url + "=")).toThrow();
62
+ expect(() => hash(alg, { enc: "base64url" }).parse(base64url + "!")).toThrow();
63
+
64
+ // Param object present but enc omitted should still default to hex at runtime
65
+ const schemaWithEmptyParams = hash(alg, {} as any);
66
+ expect(schemaWithEmptyParams.parse(hex)).toBe(hex);
67
+ }
68
+ });
@@ -84,3 +84,18 @@ test("nonoptional in object", () => {
84
84
  ]]
85
85
  `);
86
86
  });
87
+
88
+ test("encoding", () => {
89
+ const schema = z.string().optional().nonoptional();
90
+ expect(z.encode(schema, "hello")).toEqual("hello");
91
+ expect(() => z.encode(schema, undefined as any)).toThrowErrorMatchingInlineSnapshot(`
92
+ [ZodError: [
93
+ {
94
+ "code": "invalid_type",
95
+ "expected": "nonoptional",
96
+ "path": [],
97
+ "message": "Invalid input: expected nonoptional, received undefined"
98
+ }
99
+ ]]
100
+ `);
101
+ });
@@ -162,13 +162,13 @@ test("catchall overrides strict", () => {
162
162
  });
163
163
  });
164
164
 
165
- test("optional keys are unset", async () => {
165
+ test("optional keys are unset", () => {
166
166
  const SNamedEntity = z.object({
167
167
  id: z.string(),
168
168
  set: z.string().optional(),
169
169
  unset: z.string().optional(),
170
170
  });
171
- const result = await SNamedEntity.parse({
171
+ const result = SNamedEntity.parse({
172
172
  id: "asdf",
173
173
  set: undefined,
174
174
  });
@@ -435,6 +435,17 @@ test("extend() should have power to override existing key", () => {
435
435
  expectTypeOf<PersonWithNumberAsLastName>().toEqualTypeOf<{ firstName: string; lastName: number }>();
436
436
  });
437
437
 
438
+ test("safeExtend() maintains refinements", () => {
439
+ const schema = z.object({ name: z.string().min(1) });
440
+ const extended = schema.safeExtend({ name: z.string().min(2) });
441
+ expect(() => extended.parse({ name: "" })).toThrow();
442
+ expect(extended.parse({ name: "ab" })).toEqual({ name: "ab" });
443
+ type Extended = z.infer<typeof extended>;
444
+ expectTypeOf<Extended>().toEqualTypeOf<{ name: string }>();
445
+ // @ts-expect-error
446
+ schema.safeExtend({ name: z.number() });
447
+ });
448
+
438
449
  test("passthrough index signature", () => {
439
450
  const a = z.object({ a: z.string() });
440
451
  type a = z.infer<typeof a>;
@@ -576,3 +587,23 @@ test("index signature in shape", () => {
576
587
 
577
588
  expectTypeOf<schema>().toEqualTypeOf<Record<string, string>>();
578
589
  });
590
+
591
+ test("extent() on object with refinements should throw", () => {
592
+ const schema = z
593
+ .object({
594
+ a: z.string(),
595
+ })
596
+ .refine(() => true);
597
+
598
+ expect(() => schema.extend({ b: z.string() })).toThrow();
599
+ });
600
+
601
+ test("safeExtend() on object with refinements should not throw", () => {
602
+ const schema = z
603
+ .object({
604
+ a: z.string(),
605
+ })
606
+ .refine(() => true);
607
+
608
+ expect(() => schema.safeExtend({ b: z.string() })).not.toThrow();
609
+ });
@@ -45,11 +45,6 @@ test("continue on non-fatal errors", () => {
45
45
  "code": "custom",
46
46
  "path": [],
47
47
  "message": "A"
48
- },
49
- {
50
- "code": "custom",
51
- "path": [],
52
- "message": "B"
53
48
  }
54
49
  ]],
55
50
  "success": false,
@@ -79,3 +74,28 @@ test("break on fatal errors", () => {
79
74
  }
80
75
  `);
81
76
  });
77
+
78
+ test("reverse parsing with pipe", () => {
79
+ const schema = z.string().pipe(z.string());
80
+
81
+ // Reverse direction: default should NOT be applied
82
+ expect(z.safeDecode(schema, "asdf")).toMatchInlineSnapshot(`
83
+ {
84
+ "data": "asdf",
85
+ "success": true,
86
+ }
87
+ `);
88
+ expect(z.safeEncode(schema, "asdf")).toMatchInlineSnapshot(`
89
+ {
90
+ "data": "asdf",
91
+ "success": true,
92
+ }
93
+ `);
94
+ });
95
+
96
+ test("reverse parsing with pipe", () => {
97
+ const schema = z.string().transform((val) => val.length);
98
+
99
+ // should throw
100
+ expect(() => z.encode(schema, 1234)).toThrow();
101
+ });
@@ -47,3 +47,28 @@ test("object schema with prefault should return shallow clone", () => {
47
47
  expect(result1).not.toBe(result2);
48
48
  expect(result1).toEqual(result2);
49
49
  });
50
+
51
+ test("direction-aware prefault", () => {
52
+ const schema = z.string().prefault("hello");
53
+
54
+ // Forward direction (regular parse): prefault should be applied
55
+ expect(schema.parse(undefined)).toBe("hello");
56
+
57
+ // Reverse direction (encode): prefault should NOT be applied, undefined should fail validation
58
+ expect(z.safeEncode(schema, undefined as any)).toMatchInlineSnapshot(`
59
+ {
60
+ "error": [ZodError: [
61
+ {
62
+ "expected": "string",
63
+ "code": "invalid_type",
64
+ "path": [],
65
+ "message": "Invalid input: expected string, received undefined"
66
+ }
67
+ ]],
68
+ "success": false,
69
+ }
70
+ `);
71
+
72
+ // But valid values should still work in reverse
73
+ expect(z.encode(schema, "world")).toBe("world");
74
+ });
@@ -269,14 +269,9 @@ test("perform transform with non-fatal issues", () => {
269
269
  .transform((val) => val.length)
270
270
  .pipe(z.number())
271
271
  .refine((_) => false);
272
- expect(A.safeParse("asdfasdf").error!.issues).toHaveLength(2);
272
+ expect(A.safeParse("asdfasdf").error!.issues).toHaveLength(1);
273
273
  expect(A.safeParse("asdfasdf").error).toMatchInlineSnapshot(`
274
274
  [ZodError: [
275
- {
276
- "code": "custom",
277
- "path": [],
278
- "message": "Invalid input"
279
- },
280
275
  {
281
276
  "code": "custom",
282
277
  "path": [],
@@ -167,8 +167,8 @@ describe("early termination options", () => {
167
167
  });
168
168
 
169
169
  describe("custom error paths", () => {
170
- test("should use custom path in error message", async () => {
171
- const result = await z
170
+ test("should use custom path in error message", () => {
171
+ const result = z
172
172
  .object({ password: z.string(), confirm: z.string() })
173
173
  .refine((data) => data.confirm === data.password, { path: ["confirm"] })
174
174
  .safeParse({ password: "asdf", confirm: "qewr" });
@@ -252,6 +252,79 @@ describe("superRefine functionality", () => {
252
252
  await expect(Strings.parseAsync(validArray)).resolves.toEqual(validArray);
253
253
  });
254
254
 
255
+ test("should test continuability of custom issues", () => {
256
+ // Default continue behavior - allows subsequent refinements
257
+ const defaultContinue = z
258
+ .string()
259
+ .superRefine((_, ctx) => {
260
+ ctx.addIssue({ code: "custom", message: "First issue" });
261
+ })
262
+ .refine(() => false, "Second issue");
263
+
264
+ expect(defaultContinue.safeParse("test")).toMatchInlineSnapshot(`
265
+ {
266
+ "error": [ZodError: [
267
+ {
268
+ "code": "custom",
269
+ "message": "First issue",
270
+ "path": []
271
+ },
272
+ {
273
+ "code": "custom",
274
+ "path": [],
275
+ "message": "Second issue"
276
+ }
277
+ ]],
278
+ "success": false,
279
+ }
280
+ `);
281
+
282
+ // Explicit continue: false - prevents subsequent refinements
283
+ const explicitContinueFalse = z
284
+ .string()
285
+ .superRefine((_, ctx) => {
286
+ ctx.addIssue({ code: "custom", message: "First issue", continue: false });
287
+ })
288
+ .refine(() => false, "Second issue");
289
+
290
+ expect(explicitContinueFalse.safeParse("test")).toMatchInlineSnapshot(`
291
+ {
292
+ "error": [ZodError: [
293
+ {
294
+ "code": "custom",
295
+ "message": "First issue",
296
+ "path": []
297
+ }
298
+ ]],
299
+ "success": false,
300
+ }
301
+ `);
302
+
303
+ // Multiple issues in same refinement - both always added regardless of continue
304
+ const multipleInSame = z.string().superRefine((_, ctx) => {
305
+ ctx.addIssue({ code: "custom", message: "First", continue: false });
306
+ ctx.addIssue({ code: "custom", message: "Second" });
307
+ });
308
+
309
+ expect(multipleInSame.safeParse("test")).toMatchInlineSnapshot(`
310
+ {
311
+ "error": [ZodError: [
312
+ {
313
+ "code": "custom",
314
+ "message": "First",
315
+ "path": []
316
+ },
317
+ {
318
+ "code": "custom",
319
+ "message": "Second",
320
+ "path": []
321
+ }
322
+ ]],
323
+ "success": false,
324
+ }
325
+ `);
326
+ });
327
+
255
328
  test("should accept string as shorthand for custom error message", () => {
256
329
  const schema = z.string().superRefine((_, ctx) => {
257
330
  ctx.addIssue("bad stuff");
@@ -431,9 +504,9 @@ test("when", () => {
431
504
  })
432
505
  .refine(
433
506
  (data) => {
434
- console.log("running check...");
435
- console.log(data);
436
- console.log(data.password);
507
+ // console.log("running check...");
508
+ // console.log(data);
509
+ // console.log(data.password);
437
510
  return data.password === data.confirmPassword;
438
511
  },
439
512
  {
@@ -39,7 +39,7 @@ test("valid parse async", async () => {
39
39
  expect(result.data!.has("second")).toEqual(true);
40
40
  expect(result.data!.has("third")).toEqual(false);
41
41
 
42
- const asyncResult = await stringSet.safeParse(new Set(["first", "second"]));
42
+ const asyncResult = stringSet.safeParse(new Set(["first", "second"]));
43
43
  expect(asyncResult.success).toEqual(true);
44
44
  expect(asyncResult.data!.has("first")).toEqual(true);
45
45
  expect(asyncResult.data!.has("second")).toEqual(true);
@@ -107,3 +107,19 @@ test("z.stringFormat", () => {
107
107
  `/\\^\\(\\?:\\\\d\\{14,19\\}\\|\\\\d\\{4\\}\\(\\?: \\\\d\\{3,6\\}\\)\\{2,4\\}\\|\\\\d\\{4\\}\\(\\?:-\\\\d\\{3,6\\}\\)\\{2,4\\}\\)\\$/u`
108
108
  );
109
109
  });
110
+
111
+ test("z.hex", () => {
112
+ const hexSchema = z.hex();
113
+
114
+ // Valid hex strings
115
+ expect(hexSchema.safeParse("").success).toBe(true); // Empty string is valid hex
116
+ expect(hexSchema.safeParse("123abc").success).toBe(true);
117
+ expect(hexSchema.safeParse("DEADBEEF").success).toBe(true);
118
+ expect(hexSchema.safeParse("0123456789abcdefABCDEF").success).toBe(true);
119
+
120
+ // Invalid hex strings
121
+ expect(hexSchema.safeParse("xyz").success).toBe(false);
122
+ expect(hexSchema.safeParse("123g").success).toBe(false);
123
+ expect(hexSchema.safeParse("hello world").success).toBe(false);
124
+ expect(hexSchema.safeParse("123-abc").success).toBe(false);
125
+ });
@@ -527,6 +527,7 @@ test("good uuid", () => {
527
527
  "9491d710-3185-5e06-8ea0-6a2f275345e0",
528
528
  "9491d710-3185-5e06-9ea0-6a2f275345e0",
529
529
  "00000000-0000-0000-0000-000000000000",
530
+ "ffffffff-ffff-ffff-ffff-ffffffffffff",
530
531
  ];
531
532
 
532
533
  for (const goodUuid of goodUuids) {
@@ -545,7 +546,6 @@ test(`bad uuid`, () => {
545
546
  "92e76bf9-28b3-4730-cd7f-cb6bc51f8c09", // Variant 2 - RFC 9562/4122: Reserved, Microsoft Corporation backward compatibility
546
547
  "invalid uuid",
547
548
  "9491d710-3185-4e06-bea0-6a2f275345e0X",
548
- "ffffffff-ffff-ffff-ffff-ffffffffffff",
549
549
  ]) {
550
550
  const result = uuid.safeParse(badUuid);
551
551
  expect(result).toMatchObject({ success: false });
@@ -994,3 +994,84 @@ test("hostname", () => {
994
994
  expect(() => hostname.parse("-example.com")).toThrow();
995
995
  expect(() => hostname.parse("example..com")).toThrow();
996
996
  });
997
+
998
+ test("hash validation", () => {
999
+ // MD5 tests
1000
+ const md5hex = z.hash("md5");
1001
+ const md5base64 = z.hash("md5", { enc: "base64" });
1002
+ const md5base64url = z.hash("md5", { enc: "base64url" });
1003
+
1004
+ // Valid MD5 hashes
1005
+ expect(md5hex.parse("5d41402abc4b2a76b9719d911017c592")).toBe("5d41402abc4b2a76b9719d911017c592");
1006
+ expect(md5hex.parse("5D41402ABC4B2A76B9719D911017C592")).toBe("5D41402ABC4B2A76B9719D911017C592"); // uppercase
1007
+ expect(md5base64.parse("XUFAKrxLKna5cZ2REBfFkg==")).toBe("XUFAKrxLKna5cZ2REBfFkg==");
1008
+ expect(md5base64url.parse("XUFAKrxLKna5cZ2REBfFkg")).toBe("XUFAKrxLKna5cZ2REBfFkg");
1009
+
1010
+ // Invalid MD5 hashes
1011
+ expect(() => md5hex.parse("5d41402abc4b2a76b9719d911017c59")).toThrow(); // too short
1012
+ expect(() => md5hex.parse("5d41402abc4b2a76b9719d911017c592x")).toThrow(); // too long
1013
+ expect(() => md5base64.parse("XUFAKrxLKna5cZ2REBfFkg=")).toThrow(); // wrong padding
1014
+ expect(() => md5base64url.parse("XUFAKrxLKna5cZ2REBfFkg=")).toThrow(); // has padding
1015
+
1016
+ // SHA1 tests
1017
+ const sha1hex = z.hash("sha1");
1018
+ const sha1base64 = z.hash("sha1", { enc: "base64" });
1019
+ const sha1base64url = z.hash("sha1", { enc: "base64url" });
1020
+
1021
+ // Valid SHA1 hashes
1022
+ expect(sha1hex.parse("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d")).toBe("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
1023
+ expect(sha1base64.parse("qvTGHdzF6KLavt4PO0gs2a6pQ00=")).toBe("qvTGHdzF6KLavt4PO0gs2a6pQ00=");
1024
+ expect(sha1base64url.parse("qvTGHdzF6KLavt4PO0gs2a6pQ00")).toBe("qvTGHdzF6KLavt4PO0gs2a6pQ00");
1025
+
1026
+ // SHA256 tests
1027
+ const sha256hex = z.hash("sha256");
1028
+ const sha256base64 = z.hash("sha256", { enc: "base64" });
1029
+ const sha256base64url = z.hash("sha256", { enc: "base64url" });
1030
+
1031
+ // Valid SHA256 hashes
1032
+ expect(sha256hex.parse("2cf24dba4f21d4288094c4a2e2c2d6c6b0c3e0c8f0e0c8f0e0c8f0e0c8f0e0c8")).toBe(
1033
+ "2cf24dba4f21d4288094c4a2e2c2d6c6b0c3e0c8f0e0c8f0e0c8f0e0c8f0e0c8"
1034
+ );
1035
+ expect(sha256base64.parse("LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=")).toBe(
1036
+ "LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="
1037
+ );
1038
+ expect(sha256base64url.parse("LPJNul-wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ")).toBe(
1039
+ "LPJNul-wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ"
1040
+ );
1041
+
1042
+ // SHA384 tests (no padding in base64)
1043
+ const sha384hex = z.hash("sha384");
1044
+ const sha384base64 = z.hash("sha384", { enc: "base64" });
1045
+
1046
+ expect(
1047
+ sha384hex.parse("59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f")
1048
+ ).toBe("59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f");
1049
+ expect(sha384base64.parse("WeF0h3dEjGneawDXozO7+5/xtGPkQ1TDVTvNucZm+pASWjx5+QOXvfX2oT3oKGhP")).toBe(
1050
+ "WeF0h3dEjGneawDXozO7+5/xtGPkQ1TDVTvNucZm+pASWjx5+QOXvfX2oT3oKGhP"
1051
+ );
1052
+
1053
+ // SHA512 tests
1054
+ const sha512hex = z.hash("sha512");
1055
+ const sha512base64 = z.hash("sha512", { enc: "base64" });
1056
+
1057
+ expect(
1058
+ sha512hex.parse(
1059
+ "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
1060
+ )
1061
+ ).toBe(
1062
+ "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
1063
+ );
1064
+ expect(
1065
+ sha512base64.parse("m3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQw==")
1066
+ ).toBe("m3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQw==");
1067
+
1068
+ // Test default encoding (hex)
1069
+ const defaultHash = z.hash("sha256");
1070
+ expect(defaultHash.parse("2cf24dba4f21d4288094c4a2e2c2d6c6b0c3e0c8f0e0c8f0e0c8f0e0c8f0e0c8")).toBe(
1071
+ "2cf24dba4f21d4288094c4a2e2c2d6c6b0c3e0c8f0e0c8f0e0c8f0e0c8f0e0c8"
1072
+ );
1073
+
1074
+ // Test with custom error message
1075
+ const hashWithMessage = z.hash("md5", { message: "Invalid MD5 hash" });
1076
+ expect(() => hashWithMessage.parse("invalid")).toThrow("Invalid MD5 hash");
1077
+ });
@@ -64,3 +64,43 @@ test("z.stringbool with custom error messages", () => {
64
64
 
65
65
  expect(() => a.parse("")).toThrowError("wrong!");
66
66
  });
67
+
68
+ test("z.stringbool codec encoding", () => {
69
+ const schema = z.stringbool();
70
+
71
+ // Test encoding with default values
72
+ expect(z.encode(schema, true)).toEqual("true");
73
+ expect(z.encode(schema, false)).toEqual("false");
74
+ });
75
+
76
+ test("z.stringbool codec encoding with custom values", () => {
77
+ const schema = z.stringbool({
78
+ truthy: ["yes", "on", "1"],
79
+ falsy: ["no", "off", "0"],
80
+ });
81
+
82
+ // Should return first element of custom arrays
83
+ expect(z.encode(schema, true)).toEqual("yes");
84
+ expect(z.encode(schema, false)).toEqual("no");
85
+ });
86
+
87
+ test("z.stringbool codec round trip", () => {
88
+ const schema = z.stringbool({
89
+ truthy: ["enabled", "active"],
90
+ falsy: ["disabled", "inactive"],
91
+ });
92
+
93
+ // Test round trip: string -> boolean -> string
94
+ const decoded = z.decode(schema, "enabled");
95
+ expect(decoded).toEqual(true);
96
+
97
+ const encoded = z.encode(schema, decoded);
98
+ expect(encoded).toEqual("enabled"); // First element of truthy array
99
+
100
+ // Test with falsy value
101
+ const decodedFalse = z.decode(schema, "inactive");
102
+ expect(decodedFalse).toEqual(false);
103
+
104
+ const encodedFalse = z.encode(schema, decodedFalse);
105
+ expect(encodedFalse).toEqual("disabled"); // First element of falsy array
106
+ });
@@ -570,7 +570,7 @@ test("regexes", () => {
570
570
  );
571
571
  expect(ulid._zod.pattern.source).toMatchInlineSnapshot(`"^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$"`);
572
572
  expect(uuid._zod.pattern.source).toMatchInlineSnapshot(
573
- `"^([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)$"`
573
+ `"^([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)$"`
574
574
  );
575
575
  expect(stringAToZ._zod.pattern.source).toMatchInlineSnapshot(`"^[a-z]+$"`);
576
576
  expect(stringStartsWith._zod.pattern.source).toMatchInlineSnapshot(`"^hello.*$"`);
@@ -109,7 +109,7 @@ describe("toJSONSchema", () => {
109
109
  {
110
110
  "$schema": "https://json-schema.org/draft/2020-12/schema",
111
111
  "format": "uuid",
112
- "pattern": "^([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)$",
112
+ "pattern": "^([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)$",
113
113
  "type": "string",
114
114
  }
115
115
  `);
@@ -269,7 +269,7 @@ describe("toJSONSchema", () => {
269
269
  {
270
270
  "$schema": "https://json-schema.org/draft/2020-12/schema",
271
271
  "format": "uuid",
272
- "pattern": "^([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)$",
272
+ "pattern": "^([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)$",
273
273
  "type": "string",
274
274
  }
275
275
  `);
@@ -552,6 +552,25 @@ describe("toJSONSchema", () => {
552
552
  `);
553
553
  });
554
554
 
555
+ test("nullable openapi", () => {
556
+ expect(z.toJSONSchema(z.string().nullable(), { target: "openapi-3.0" })).toMatchInlineSnapshot(`
557
+ {
558
+ "nullable": true,
559
+ "type": "string",
560
+ }
561
+ `);
562
+ });
563
+
564
+ test("union with null openapi", () => {
565
+ const schema = z.union([z.string(), z.null()]);
566
+ expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
567
+ {
568
+ "nullable": true,
569
+ "type": "string",
570
+ }
571
+ `);
572
+ });
573
+
555
574
  test("arrays", () => {
556
575
  expect(z.toJSONSchema(z.array(z.string()))).toMatchInlineSnapshot(`
557
576
  {
@@ -352,3 +352,10 @@ test("do not continue by default", () => {
352
352
  }
353
353
  `);
354
354
  });
355
+
356
+ test("encode error", () => {
357
+ const schema = z.string().transform((val) => val.length);
358
+ expect(() => z.encode(schema, 1234)).toThrowErrorMatchingInlineSnapshot(
359
+ `[ZodEncodeError: Encountered unidirectional transform during encode: ZodTransform]`
360
+ );
361
+ });
@@ -157,7 +157,7 @@ test("surface continuable errors only if they exist", () => {
157
157
  "origin": "string",
158
158
  "code": "invalid_format",
159
159
  "format": "uuid",
160
- "pattern": "/^([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)$/",
160
+ "pattern": "/^([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)$/",
161
161
  "path": [],
162
162
  "message": "Invalid UUID"
163
163
  }