zod
Version:
TypeScript-first schema declaration and validation library with static type inference
314 lines (272 loc) • 8.17 kB
text/typescript
// @ts-ignore TS6133
import { expect, test } from "vitest";
import * as z from "zod/v3";
import { ZodIssueCode } from "../ZodError.js";
import { util } from "../helpers/util.js";
test("refinement", () => {
const obj1 = z.object({
first: z.string(),
second: z.string(),
});
const obj2 = obj1.partial().strict();
const obj3 = obj2.refine((data) => data.first || data.second, "Either first or second should be filled in.");
expect(obj1 === (obj2 as any)).toEqual(false);
expect(obj2 === (obj3 as any)).toEqual(false);
expect(() => obj1.parse({})).toThrow();
expect(() => obj2.parse({ third: "adsf" })).toThrow();
expect(() => obj3.parse({})).toThrow();
obj3.parse({ first: "a" });
obj3.parse({ second: "a" });
obj3.parse({ first: "a", second: "a" });
});
test("refinement 2", () => {
const validationSchema = z
.object({
email: z.string().email(),
password: z.string(),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, "Both password and confirmation must match");
expect(() =>
validationSchema.parse({
email: "aaaa@gmail.com",
password: "aaaaaaaa",
confirmPassword: "bbbbbbbb",
})
).toThrow();
});
test("refinement type guard", () => {
const validationSchema = z.object({
a: z.string().refine((s): s is "a" => s === "a"),
});
type Input = z.input<typeof validationSchema>;
type Schema = z.infer<typeof validationSchema>;
util.assertEqual<"a", Input["a"]>(false);
util.assertEqual<string, Input["a"]>(true);
util.assertEqual<"a", Schema["a"]>(true);
util.assertEqual<string, Schema["a"]>(false);
});
test("refinement Promise", async () => {
const validationSchema = z
.object({
email: z.string().email(),
password: z.string(),
confirmPassword: z.string(),
})
.refine(
(data) => Promise.resolve().then(() => data.password === data.confirmPassword),
"Both password and confirmation must match"
);
await validationSchema.parseAsync({
email: "aaaa@gmail.com",
password: "password",
confirmPassword: "password",
});
});
test("custom path", async () => {
const result = await z
.object({
password: z.string(),
confirm: z.string(),
})
.refine((data) => data.confirm === data.password, { path: ["confirm"] })
.spa({ password: "asdf", confirm: "qewr" });
expect(result.success).toEqual(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual(["confirm"]);
}
});
test("use path in refinement context", async () => {
const noNested = z.string()._refinement((_val, ctx) => {
if (ctx.path.length > 0) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `schema cannot be nested. path: ${ctx.path.join(".")}`,
});
return false;
} else {
return true;
}
});
const data = z.object({
foo: noNested,
});
const t1 = await noNested.spa("asdf");
const t2 = await data.spa({ foo: "asdf" });
expect(t1.success).toBe(true);
expect(t2.success).toBe(false);
if (t2.success === false) {
expect(t2.error.issues[0].message).toEqual("schema cannot be nested. path: foo");
}
});
test("superRefine", () => {
const Strings = z.array(z.string()).superRefine((val, ctx) => {
if (val.length > 3) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
maximum: 3,
type: "array",
inclusive: true,
exact: true,
message: "Too many items 😡",
});
}
if (val.length !== new Set(val).size) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `No duplicates allowed.`,
});
}
});
const result = Strings.safeParse(["asfd", "asfd", "asfd", "asfd"]);
expect(result.success).toEqual(false);
if (!result.success) expect(result.error.issues.length).toEqual(2);
Strings.parse(["asfd", "qwer"]);
});
test("superRefine async", async () => {
const Strings = z.array(z.string()).superRefine(async (val, ctx) => {
if (val.length > 3) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
maximum: 3,
type: "array",
inclusive: true,
exact: true,
message: "Too many items 😡",
});
}
if (val.length !== new Set(val).size) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `No duplicates allowed.`,
});
}
});
const result = await Strings.safeParseAsync(["asfd", "asfd", "asfd", "asfd"]);
expect(result.success).toEqual(false);
if (!result.success) expect(result.error.issues.length).toEqual(2);
Strings.parseAsync(["asfd", "qwer"]);
});
test("superRefine - type narrowing", () => {
type NarrowType = { type: string; age: number };
const schema = z
.object({
type: z.string(),
age: z.number(),
})
.nullable()
.superRefine((arg, ctx): arg is NarrowType => {
if (!arg) {
// still need to make a call to ctx.addIssue
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "cannot be null",
fatal: true,
});
return false;
}
return true;
});
util.assertEqual<z.infer<typeof schema>, NarrowType>(true);
expect(schema.safeParse({ type: "test", age: 0 }).success).toEqual(true);
expect(schema.safeParse(null).success).toEqual(false);
});
test("chained mixed refining types", () => {
type firstRefinement = { first: string; second: number; third: true };
type secondRefinement = { first: "bob"; second: number; third: true };
type thirdRefinement = { first: "bob"; second: 33; third: true };
const schema = z
.object({
first: z.string(),
second: z.number(),
third: z.boolean(),
})
.nullable()
.refine((arg): arg is firstRefinement => !!arg?.third)
.superRefine((arg, ctx): arg is secondRefinement => {
util.assertEqual<typeof arg, firstRefinement>(true);
if (arg.first !== "bob") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "`first` property must be `bob`",
});
return false;
}
return true;
})
.refine((arg): arg is thirdRefinement => {
util.assertEqual<typeof arg, secondRefinement>(true);
return arg.second === 33;
});
util.assertEqual<z.infer<typeof schema>, thirdRefinement>(true);
});
test("get inner type", () => {
z.string()
.refine(() => true)
.innerType()
.parse("asdf");
});
test("chained refinements", () => {
const objectSchema = z
.object({
length: z.number(),
size: z.number(),
})
.refine(({ length }) => length > 5, {
path: ["length"],
message: "length greater than 5",
})
.refine(({ size }) => size > 7, {
path: ["size"],
message: "size greater than 7",
});
const r1 = objectSchema.safeParse({
length: 4,
size: 9,
});
expect(r1.success).toEqual(false);
if (!r1.success) expect(r1.error.issues.length).toEqual(1);
const r2 = objectSchema.safeParse({
length: 4,
size: 3,
});
expect(r2.success).toEqual(false);
if (!r2.success) expect(r2.error.issues.length).toEqual(2);
});
test("fatal superRefine", () => {
const Strings = z
.string()
.superRefine((val, ctx) => {
if (val === "") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "foo",
fatal: true,
});
}
})
.superRefine((val, ctx) => {
if (val !== " ") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "bar",
});
}
});
const result = Strings.safeParse("");
expect(result.success).toEqual(false);
if (!result.success) expect(result.error.issues.length).toEqual(1);
});
test("superRefine after skipped transform", () => {
const schema = z
.string()
.regex(/^\d+$/)
.transform((val) => Number(val))
.superRefine((val) => {
if (typeof val !== "number") {
throw new Error("Called without transform");
}
});
const result = schema.safeParse("");
expect(result.success).toEqual(false);
});