zod 4.1.0-canary.20250821T014930 → 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 (103) hide show
  1. package/package.json +1 -1
  2. package/src/v4/classic/external.ts +0 -1
  3. package/src/v4/classic/parse.ts +49 -0
  4. package/src/v4/classic/schemas.ts +145 -7
  5. package/src/v4/classic/tests/catch.test.ts +25 -0
  6. package/src/v4/classic/tests/codec-examples.test.ts +538 -0
  7. package/src/v4/classic/tests/codec.test.ts +532 -0
  8. package/src/v4/classic/tests/continuability.test.ts +1 -1
  9. package/src/v4/classic/tests/default.test.ts +32 -0
  10. package/src/v4/classic/tests/firstparty.test.ts +4 -0
  11. package/src/v4/classic/tests/function.test.ts +31 -31
  12. package/src/v4/classic/tests/hash.test.ts +68 -0
  13. package/src/v4/classic/tests/nonoptional.test.ts +15 -0
  14. package/src/v4/classic/tests/object.test.ts +31 -0
  15. package/src/v4/classic/tests/pipe.test.ts +25 -5
  16. package/src/v4/classic/tests/prefault.test.ts +25 -0
  17. package/src/v4/classic/tests/preprocess.test.ts +1 -6
  18. package/src/v4/classic/tests/refine.test.ts +76 -3
  19. package/src/v4/classic/tests/string-formats.test.ts +16 -0
  20. package/src/v4/classic/tests/string.test.ts +82 -1
  21. package/src/v4/classic/tests/stringbool.test.ts +40 -0
  22. package/src/v4/classic/tests/template-literal.test.ts +1 -1
  23. package/src/v4/classic/tests/to-json-schema.test.ts +21 -2
  24. package/src/v4/classic/tests/transform.test.ts +7 -0
  25. package/src/v4/classic/tests/union.test.ts +1 -1
  26. package/src/v4/core/api.ts +25 -35
  27. package/src/v4/core/core.ts +7 -26
  28. package/src/v4/core/index.ts +0 -1
  29. package/src/v4/core/json-schema.ts +1 -0
  30. package/src/v4/core/parse.ts +101 -0
  31. package/src/v4/core/regexes.ts +40 -1
  32. package/src/v4/core/schemas.ts +521 -129
  33. package/src/v4/core/to-json-schema.ts +43 -8
  34. package/src/v4/core/util.ts +73 -0
  35. package/src/v4/mini/external.ts +0 -1
  36. package/src/v4/mini/parse.ts +14 -1
  37. package/src/v4/mini/schemas.ts +153 -12
  38. package/src/v4/mini/tests/codec.test.ts +499 -0
  39. package/src/v4/mini/tests/object.test.ts +9 -0
  40. package/src/v4/mini/tests/string.test.ts +16 -0
  41. package/v4/classic/external.cjs +1 -2
  42. package/v4/classic/external.d.cts +1 -1
  43. package/v4/classic/external.d.ts +1 -1
  44. package/v4/classic/external.js +1 -1
  45. package/v4/classic/parse.cjs +10 -1
  46. package/v4/classic/parse.d.cts +8 -0
  47. package/v4/classic/parse.d.ts +8 -0
  48. package/v4/classic/parse.js +9 -0
  49. package/v4/classic/schemas.cjs +59 -4
  50. package/v4/classic/schemas.d.cts +48 -2
  51. package/v4/classic/schemas.d.ts +48 -2
  52. package/v4/classic/schemas.js +51 -3
  53. package/v4/core/api.cjs +19 -24
  54. package/v4/core/api.d.cts +3 -4
  55. package/v4/core/api.d.ts +3 -4
  56. package/v4/core/api.js +19 -24
  57. package/v4/core/core.cjs +8 -1
  58. package/v4/core/core.d.cts +3 -0
  59. package/v4/core/core.d.ts +3 -0
  60. package/v4/core/core.js +6 -0
  61. package/v4/core/index.cjs +0 -1
  62. package/v4/core/index.d.cts +0 -1
  63. package/v4/core/index.d.ts +0 -1
  64. package/v4/core/index.js +0 -1
  65. package/v4/core/json-schema.d.cts +1 -0
  66. package/v4/core/json-schema.d.ts +1 -0
  67. package/v4/core/parse.cjs +45 -1
  68. package/v4/core/parse.d.cts +24 -0
  69. package/v4/core/parse.d.ts +24 -0
  70. package/v4/core/parse.js +36 -0
  71. package/v4/core/regexes.cjs +34 -2
  72. package/v4/core/regexes.d.cts +16 -0
  73. package/v4/core/regexes.d.ts +16 -0
  74. package/v4/core/regexes.js +32 -1
  75. package/v4/core/schemas.cjs +309 -77
  76. package/v4/core/schemas.d.cts +61 -3
  77. package/v4/core/schemas.d.ts +61 -3
  78. package/v4/core/schemas.js +308 -76
  79. package/v4/core/to-json-schema.cjs +42 -5
  80. package/v4/core/to-json-schema.d.cts +4 -3
  81. package/v4/core/to-json-schema.d.ts +4 -3
  82. package/v4/core/to-json-schema.js +42 -5
  83. package/v4/core/util.cjs +69 -0
  84. package/v4/core/util.d.cts +10 -0
  85. package/v4/core/util.d.ts +10 -0
  86. package/v4/core/util.js +62 -0
  87. package/v4/mini/external.cjs +1 -2
  88. package/v4/mini/external.d.cts +1 -1
  89. package/v4/mini/external.d.ts +1 -1
  90. package/v4/mini/external.js +1 -1
  91. package/v4/mini/parse.cjs +9 -1
  92. package/v4/mini/parse.d.cts +1 -1
  93. package/v4/mini/parse.d.ts +1 -1
  94. package/v4/mini/parse.js +1 -1
  95. package/v4/mini/schemas.cjs +58 -3
  96. package/v4/mini/schemas.d.cts +49 -1
  97. package/v4/mini/schemas.d.ts +49 -1
  98. package/v4/mini/schemas.js +49 -2
  99. package/src/v4/core/function.ts +0 -176
  100. package/v4/core/function.cjs +0 -102
  101. package/v4/core/function.d.cts +0 -52
  102. package/v4/core/function.d.ts +0 -52
  103. 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
+ });
@@ -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": [],
@@ -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
  {
@@ -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
  }
@@ -1476,7 +1476,7 @@ type RawIssue<T extends errors.$ZodIssueBase> = T extends any
1476
1476
  util.MakePartial<T, "message" | "path"> & {
1477
1477
  /** The schema or check that originated this issue. */
1478
1478
  readonly inst?: schemas.$ZodType | checks.$ZodCheck;
1479
- /** If `true`, Zod will continue executing checks/refinements after this issue. */
1479
+ /** If `true`, Zod will execute subsequent checks/refinements instead of immediately aborting */
1480
1480
  readonly continue?: boolean | undefined;
1481
1481
  } & Record<string, unknown>
1482
1482
  >
@@ -1498,7 +1498,7 @@ export function _superRefine<T>(fn: (arg: T, payload: $RefinementCtx<T>) => void
1498
1498
  _issue.code ??= "custom";
1499
1499
  _issue.input ??= payload.value;
1500
1500
  _issue.inst ??= ch;
1501
- _issue.continue ??= !ch._zod.def.abort;
1501
+ _issue.continue ??= !ch._zod.def.abort; // abort is always undefined, so this is always true...
1502
1502
  payload.issues.push(util.issue(_issue));
1503
1503
  }
1504
1504
  };
@@ -1536,16 +1536,12 @@ export interface $ZodStringBoolParams extends TypeParams {
1536
1536
 
1537
1537
  export function _stringbool(
1538
1538
  Classes: {
1539
- Pipe?: typeof schemas.$ZodPipe;
1539
+ Codec?: typeof schemas.$ZodCodec;
1540
1540
  Boolean?: typeof schemas.$ZodBoolean;
1541
- Transform?: typeof schemas.$ZodTransform;
1542
1541
  String?: typeof schemas.$ZodString;
1543
1542
  },
1544
1543
  _params?: string | $ZodStringBoolParams
1545
- ): schemas.$ZodPipe<
1546
- schemas.$ZodPipe<schemas.$ZodString, schemas.$ZodTransform<boolean, string>>,
1547
- schemas.$ZodBoolean<boolean>
1548
- > {
1544
+ ): schemas.$ZodCodec<schemas.$ZodString, schemas.$ZodBoolean> {
1549
1545
  const params = util.normalizeParams(_params);
1550
1546
 
1551
1547
  let truthyArray = params.truthy ?? ["true", "1", "yes", "on", "y", "enabled"];
@@ -1558,15 +1554,19 @@ export function _stringbool(
1558
1554
  const truthySet = new Set(truthyArray);
1559
1555
  const falsySet = new Set(falsyArray);
1560
1556
 
1561
- const _Pipe = Classes.Pipe ?? schemas.$ZodPipe;
1557
+ const _Codec = Classes.Codec ?? schemas.$ZodCodec;
1562
1558
  const _Boolean = Classes.Boolean ?? schemas.$ZodBoolean;
1563
1559
  const _String = Classes.String ?? schemas.$ZodString;
1564
- const _Transform = Classes.Transform ?? schemas.$ZodTransform;
1565
1560
 
1566
- const tx = new _Transform({
1567
- type: "transform",
1568
- transform: (input, payload: schemas.ParsePayload<unknown>) => {
1569
- let data: string = input as string;
1561
+ const stringSchema = new _String({ type: "string", error: params.error });
1562
+ const booleanSchema = new _Boolean({ type: "boolean", error: params.error });
1563
+
1564
+ const codec = new _Codec({
1565
+ type: "pipe",
1566
+ in: stringSchema as any,
1567
+ out: booleanSchema as any,
1568
+ transform: ((input: string, payload: schemas.ParsePayload<string>) => {
1569
+ let data: string = input;
1570
1570
  if (params.case !== "sensitive") data = data.toLowerCase();
1571
1571
  if (truthySet.has(data)) {
1572
1572
  return true;
@@ -1578,33 +1578,23 @@ export function _stringbool(
1578
1578
  expected: "stringbool",
1579
1579
  values: [...truthySet, ...falsySet],
1580
1580
  input: payload.value,
1581
- inst: tx,
1581
+ inst: codec,
1582
1582
  continue: false,
1583
1583
  });
1584
1584
  return {} as never;
1585
1585
  }
1586
- },
1587
- error: params.error,
1588
- });
1589
- // params.error;
1590
-
1591
- const innerPipe = new _Pipe({
1592
- type: "pipe",
1593
- in: new _String({ type: "string", error: params.error }),
1594
- out: tx,
1586
+ }) as any,
1587
+ reverseTransform: ((input: boolean, _payload: schemas.ParsePayload<boolean>) => {
1588
+ if (input === true) {
1589
+ return truthyArray[0] || "true";
1590
+ } else {
1591
+ return falsyArray[0] || "false";
1592
+ }
1593
+ }) as any,
1595
1594
  error: params.error,
1596
- });
1595
+ }) as any;
1597
1596
 
1598
- const outerPipe = new _Pipe({
1599
- type: "pipe",
1600
- in: innerPipe,
1601
- out: new _Boolean({
1602
- type: "boolean",
1603
- error: params.error,
1604
- }),
1605
- error: params.error,
1606
- });
1607
- return outerPipe as any;
1597
+ return codec;
1608
1598
  }
1609
1599
 
1610
1600
  export function _stringFormat<Format extends string>(