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.
- package/package.json +1 -1
- package/src/v3/tests/object.test.ts +2 -2
- 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/array.test.ts +6 -6
- 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 +33 -2
- 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 +78 -5
- package/src/v4/classic/tests/set.test.ts +1 -1
- 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
|
+
});
|
|
@@ -162,13 +162,13 @@ test("catchall overrides strict", () => {
|
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
test("optional keys are unset",
|
|
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 =
|
|
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(
|
|
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",
|
|
171
|
-
const result =
|
|
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 =
|
|
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
|
}
|