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.
- package/package.json +1 -1
- package/src/v4/classic/external.ts +0 -1
- package/src/v4/classic/parse.ts +49 -0
- package/src/v4/classic/schemas.ts +145 -7
- package/src/v4/classic/tests/catch.test.ts +25 -0
- package/src/v4/classic/tests/codec-examples.test.ts +538 -0
- package/src/v4/classic/tests/codec.test.ts +532 -0
- package/src/v4/classic/tests/continuability.test.ts +1 -1
- package/src/v4/classic/tests/default.test.ts +32 -0
- package/src/v4/classic/tests/firstparty.test.ts +4 -0
- package/src/v4/classic/tests/function.test.ts +31 -31
- package/src/v4/classic/tests/hash.test.ts +68 -0
- package/src/v4/classic/tests/nonoptional.test.ts +15 -0
- package/src/v4/classic/tests/object.test.ts +31 -0
- package/src/v4/classic/tests/pipe.test.ts +25 -5
- package/src/v4/classic/tests/prefault.test.ts +25 -0
- package/src/v4/classic/tests/preprocess.test.ts +1 -6
- package/src/v4/classic/tests/refine.test.ts +76 -3
- package/src/v4/classic/tests/string-formats.test.ts +16 -0
- package/src/v4/classic/tests/string.test.ts +82 -1
- package/src/v4/classic/tests/stringbool.test.ts +40 -0
- package/src/v4/classic/tests/template-literal.test.ts +1 -1
- package/src/v4/classic/tests/to-json-schema.test.ts +21 -2
- package/src/v4/classic/tests/transform.test.ts +7 -0
- package/src/v4/classic/tests/union.test.ts +1 -1
- package/src/v4/core/api.ts +25 -35
- package/src/v4/core/core.ts +7 -26
- package/src/v4/core/index.ts +0 -1
- package/src/v4/core/json-schema.ts +1 -0
- package/src/v4/core/parse.ts +101 -0
- package/src/v4/core/regexes.ts +40 -1
- package/src/v4/core/schemas.ts +521 -129
- package/src/v4/core/to-json-schema.ts +43 -8
- package/src/v4/core/util.ts +73 -0
- package/src/v4/mini/external.ts +0 -1
- package/src/v4/mini/parse.ts +14 -1
- package/src/v4/mini/schemas.ts +153 -12
- package/src/v4/mini/tests/codec.test.ts +499 -0
- package/src/v4/mini/tests/object.test.ts +9 -0
- package/src/v4/mini/tests/string.test.ts +16 -0
- package/v4/classic/external.cjs +1 -2
- package/v4/classic/external.d.cts +1 -1
- package/v4/classic/external.d.ts +1 -1
- package/v4/classic/external.js +1 -1
- package/v4/classic/parse.cjs +10 -1
- package/v4/classic/parse.d.cts +8 -0
- package/v4/classic/parse.d.ts +8 -0
- package/v4/classic/parse.js +9 -0
- package/v4/classic/schemas.cjs +59 -4
- package/v4/classic/schemas.d.cts +48 -2
- package/v4/classic/schemas.d.ts +48 -2
- package/v4/classic/schemas.js +51 -3
- package/v4/core/api.cjs +19 -24
- package/v4/core/api.d.cts +3 -4
- package/v4/core/api.d.ts +3 -4
- package/v4/core/api.js +19 -24
- package/v4/core/core.cjs +8 -1
- package/v4/core/core.d.cts +3 -0
- package/v4/core/core.d.ts +3 -0
- package/v4/core/core.js +6 -0
- package/v4/core/index.cjs +0 -1
- package/v4/core/index.d.cts +0 -1
- package/v4/core/index.d.ts +0 -1
- package/v4/core/index.js +0 -1
- package/v4/core/json-schema.d.cts +1 -0
- package/v4/core/json-schema.d.ts +1 -0
- package/v4/core/parse.cjs +45 -1
- package/v4/core/parse.d.cts +24 -0
- package/v4/core/parse.d.ts +24 -0
- package/v4/core/parse.js +36 -0
- package/v4/core/regexes.cjs +34 -2
- package/v4/core/regexes.d.cts +16 -0
- package/v4/core/regexes.d.ts +16 -0
- package/v4/core/regexes.js +32 -1
- package/v4/core/schemas.cjs +309 -77
- package/v4/core/schemas.d.cts +61 -3
- package/v4/core/schemas.d.ts +61 -3
- package/v4/core/schemas.js +308 -76
- package/v4/core/to-json-schema.cjs +42 -5
- package/v4/core/to-json-schema.d.cts +4 -3
- package/v4/core/to-json-schema.d.ts +4 -3
- package/v4/core/to-json-schema.js +42 -5
- package/v4/core/util.cjs +69 -0
- package/v4/core/util.d.cts +10 -0
- package/v4/core/util.d.ts +10 -0
- package/v4/core/util.js +62 -0
- package/v4/mini/external.cjs +1 -2
- package/v4/mini/external.d.cts +1 -1
- package/v4/mini/external.d.ts +1 -1
- package/v4/mini/external.js +1 -1
- package/v4/mini/parse.cjs +9 -1
- package/v4/mini/parse.d.cts +1 -1
- package/v4/mini/parse.d.ts +1 -1
- package/v4/mini/parse.js +1 -1
- package/v4/mini/schemas.cjs +58 -3
- package/v4/mini/schemas.d.cts +49 -1
- package/v4/mini/schemas.d.ts +49 -1
- package/v4/mini/schemas.js +49 -2
- package/src/v4/core/function.ts +0 -176
- package/v4/core/function.cjs +0 -102
- package/v4/core/function.d.cts +0 -52
- package/v4/core/function.d.ts +0 -52
- 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(
|
|
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
|
}
|
package/src/v4/core/api.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.$
|
|
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
|
|
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
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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:
|
|
1581
|
+
inst: codec,
|
|
1582
1582
|
continue: false,
|
|
1583
1583
|
});
|
|
1584
1584
|
return {} as never;
|
|
1585
1585
|
}
|
|
1586
|
-
},
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
-
|
|
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>(
|