379 lines
8.5 KiB
Plaintext
379 lines
8.5 KiB
Plaintext
import { expect, expectTypeOf, test } from "vitest";
|
|
import * as z from "zod/v4";
|
|
|
|
test("transform ctx.addIssue with parse", () => {
|
|
const strs = ["foo", "bar"];
|
|
const schema = z.string().transform((data, ctx) => {
|
|
const i = strs.indexOf(data);
|
|
if (i === -1) {
|
|
ctx.addIssue({
|
|
input: data,
|
|
code: "custom",
|
|
message: `${data} is not one of our allowed strings`,
|
|
});
|
|
}
|
|
return data.length;
|
|
});
|
|
const result = schema.safeParse("asdf");
|
|
expect(result.success).toEqual(false);
|
|
expect(result.error!).toMatchInlineSnapshot(`
|
|
[ZodError: [
|
|
{
|
|
"code": "custom",
|
|
"message": "asdf is not one of our allowed strings",
|
|
"path": []
|
|
}
|
|
]]
|
|
`);
|
|
});
|
|
|
|
test("transform ctx.addIssue with parseAsync", async () => {
|
|
const strs = ["foo", "bar"];
|
|
|
|
const result = await z
|
|
.string()
|
|
.transform(async (data, ctx) => {
|
|
const i = strs.indexOf(data);
|
|
if (i === -1) {
|
|
ctx.addIssue({
|
|
input: data,
|
|
code: "custom",
|
|
message: `${data} is not one of our allowed strings`,
|
|
});
|
|
}
|
|
return data.length;
|
|
})
|
|
.safeParseAsync("asdf");
|
|
|
|
expect(result).toMatchInlineSnapshot(`
|
|
{
|
|
"error": [ZodError: [
|
|
{
|
|
"code": "custom",
|
|
"message": "asdf is not one of our allowed strings",
|
|
"path": []
|
|
}
|
|
]],
|
|
"success": false,
|
|
}
|
|
`);
|
|
});
|
|
|
|
test("z.NEVER in transform", () => {
|
|
const foo = z
|
|
.number()
|
|
.optional()
|
|
.transform((val, ctx) => {
|
|
if (!val) {
|
|
ctx.addIssue({
|
|
input: val,
|
|
code: z.ZodIssueCode.custom,
|
|
message: "bad",
|
|
});
|
|
return z.NEVER;
|
|
}
|
|
return val;
|
|
});
|
|
type foo = z.infer<typeof foo>;
|
|
expectTypeOf<foo>().toEqualTypeOf<number>();
|
|
const arg = foo.safeParse(undefined);
|
|
if (!arg.success) {
|
|
expect(arg.error.issues[0].message).toEqual("bad");
|
|
}
|
|
});
|
|
|
|
test("basic transformations", () => {
|
|
const r1 = z
|
|
.string()
|
|
.transform((data) => data.length)
|
|
.parse("asdf");
|
|
expect(r1).toEqual(4);
|
|
});
|
|
|
|
test("coercion", () => {
|
|
const numToString = z.number().transform((n) => String(n));
|
|
const data = z
|
|
.object({
|
|
id: numToString,
|
|
})
|
|
.parse({ id: 5 });
|
|
|
|
expect(data).toEqual({ id: "5" });
|
|
});
|
|
|
|
test("async coercion", async () => {
|
|
const numToString = z.number().transform(async (n) => String(n));
|
|
const data = await z
|
|
.object({
|
|
id: numToString,
|
|
})
|
|
.parseAsync({ id: 5 });
|
|
|
|
expect(data).toEqual({ id: "5" });
|
|
});
|
|
|
|
test("sync coercion async error", async () => {
|
|
const asyncNumberToString = z.number().transform(async (n) => String(n));
|
|
expect(() =>
|
|
z
|
|
.object({
|
|
id: asyncNumberToString,
|
|
})
|
|
.parse({ id: 5 })
|
|
).toThrow();
|
|
// expect(data).toEqual({ id: '5' });
|
|
});
|
|
|
|
test("default", () => {
|
|
const data = z.string().default("asdf").parse(undefined); // => "asdf"
|
|
expect(data).toEqual("asdf");
|
|
});
|
|
|
|
test("dynamic default", () => {
|
|
const data = z
|
|
.string()
|
|
.default(() => "string")
|
|
.parse(undefined); // => "asdf"
|
|
expect(data).toEqual("string");
|
|
});
|
|
|
|
test("default when property is null or undefined", () => {
|
|
const data = z
|
|
.object({
|
|
foo: z.boolean().nullable().default(true),
|
|
bar: z.boolean().default(true),
|
|
})
|
|
.parse({ foo: null });
|
|
|
|
expect(data).toEqual({ foo: null, bar: true });
|
|
});
|
|
|
|
test("default with falsy values", () => {
|
|
const schema = z.object({
|
|
emptyStr: z.string().default("def"),
|
|
zero: z.number().default(5),
|
|
falseBoolean: z.boolean().default(true),
|
|
});
|
|
const input = { emptyStr: "", zero: 0, falseBoolean: true };
|
|
const output = schema.parse(input);
|
|
// defaults are not supposed to be used
|
|
expect(output).toEqual(input);
|
|
});
|
|
|
|
test("object typing", () => {
|
|
const stringToNumber = z.string().transform((arg) => Number.parseFloat(arg));
|
|
|
|
const t1 = z.object({
|
|
stringToNumber,
|
|
});
|
|
|
|
type t1 = z.input<typeof t1>;
|
|
type t2 = z.output<typeof t1>;
|
|
|
|
expectTypeOf<t1>().toEqualTypeOf<{ stringToNumber: string }>();
|
|
expectTypeOf<t2>().toEqualTypeOf<{ stringToNumber: number }>();
|
|
});
|
|
|
|
test("transform method overloads", () => {
|
|
const t1 = z.string().transform((val) => val.toUpperCase());
|
|
expect(t1.parse("asdf")).toEqual("ASDF");
|
|
|
|
const t2 = z.string().transform((val) => val.length);
|
|
expect(t2.parse("asdf")).toEqual(4);
|
|
});
|
|
|
|
test("multiple transformers", () => {
|
|
const stringToNumber = z.string().transform((arg) => Number.parseFloat(arg));
|
|
|
|
const doubler = stringToNumber.transform((val) => {
|
|
return val * 2;
|
|
});
|
|
expect(doubler.parse("5")).toEqual(10);
|
|
});
|
|
|
|
test("short circuit on dirty", () => {
|
|
const schema = z
|
|
.string()
|
|
.refine(() => false)
|
|
.transform((val) => val.toUpperCase());
|
|
const result = schema.safeParse("asdf");
|
|
expect(result.success).toEqual(false);
|
|
|
|
expect(result.error).toMatchInlineSnapshot(`
|
|
[ZodError: [
|
|
{
|
|
"code": "custom",
|
|
"path": [],
|
|
"message": "Invalid input"
|
|
}
|
|
]]
|
|
`);
|
|
|
|
const result2 = schema.safeParse(1234);
|
|
expect(result2.success).toEqual(false);
|
|
if (!result2.success) {
|
|
expect(result2.error.issues[0].code).toEqual(z.ZodIssueCode.invalid_type);
|
|
}
|
|
});
|
|
|
|
test("async short circuit on dirty", async () => {
|
|
const schema = z
|
|
.string()
|
|
.refine(() => false)
|
|
.transform((val) => val.toUpperCase());
|
|
const result = await schema.spa("asdf");
|
|
expect(result.success).toEqual(false);
|
|
|
|
expect(result.error).toMatchInlineSnapshot(`
|
|
[ZodError: [
|
|
{
|
|
"code": "custom",
|
|
"path": [],
|
|
"message": "Invalid input"
|
|
}
|
|
]]
|
|
`);
|
|
|
|
const result2 = await schema.spa(1234);
|
|
expect(result2.success).toEqual(false);
|
|
|
|
expect(result2.error).toMatchInlineSnapshot(`
|
|
[ZodError: [
|
|
{
|
|
"expected": "string",
|
|
"code": "invalid_type",
|
|
"path": [],
|
|
"message": "Invalid input: expected string, received number"
|
|
}
|
|
]]
|
|
`);
|
|
});
|
|
|
|
test("do not continue by default", () => {
|
|
const A = z
|
|
.string()
|
|
.transform((val, ctx) => {
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: `custom error`,
|
|
});
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: `custom error`,
|
|
});
|
|
return val;
|
|
})
|
|
.pipe(z.number() as any);
|
|
expect(A.safeParse("asdf")).toMatchInlineSnapshot(`
|
|
{
|
|
"error": [ZodError: [
|
|
{
|
|
"code": "custom",
|
|
"message": "custom error",
|
|
"path": []
|
|
},
|
|
{
|
|
"code": "custom",
|
|
"message": "custom error",
|
|
"path": []
|
|
}
|
|
]],
|
|
"success": false,
|
|
}
|
|
`);
|
|
|
|
const B = z
|
|
.string()
|
|
.transform((val, ctx) => {
|
|
ctx.issues.push({
|
|
code: "custom",
|
|
message: `custom error`,
|
|
input: val,
|
|
});
|
|
ctx.issues.push({
|
|
code: "custom",
|
|
message: `custom error`,
|
|
input: val,
|
|
});
|
|
return val;
|
|
})
|
|
.pipe(z.number() as any);
|
|
expect(B.safeParse("asdf")).toMatchInlineSnapshot(`
|
|
{
|
|
"error": [ZodError: [
|
|
{
|
|
"code": "custom",
|
|
"message": "custom error",
|
|
"path": []
|
|
},
|
|
{
|
|
"code": "custom",
|
|
"message": "custom error",
|
|
"path": []
|
|
}
|
|
]],
|
|
"success": false,
|
|
}
|
|
`);
|
|
|
|
const C = z
|
|
.string()
|
|
.transform((val, ctx) => {
|
|
ctx.issues.push({
|
|
code: "custom",
|
|
message: `custom error`,
|
|
input: val,
|
|
continue: true,
|
|
});
|
|
ctx.issues.push({
|
|
code: "custom",
|
|
message: `custom error`,
|
|
input: val,
|
|
continue: true,
|
|
});
|
|
return val;
|
|
})
|
|
.pipe(z.number() as any);
|
|
expect(C.safeParse("asdf")).toMatchInlineSnapshot(`
|
|
{
|
|
"error": [ZodError: [
|
|
{
|
|
"code": "custom",
|
|
"message": "custom error",
|
|
"path": []
|
|
},
|
|
{
|
|
"code": "custom",
|
|
"message": "custom error",
|
|
"path": []
|
|
}
|
|
]],
|
|
"success": false,
|
|
}
|
|
`);
|
|
});
|
|
|
|
test("encode error", () => {
|
|
const schema = z.string().transform((val) => val.length);
|
|
expect(() => z.encode(schema, 1234)).toThrowErrorMatchingInlineSnapshot(
|
|
`[ZodEncodeError: Encountered unidirectional transform during encode: ZodTransform]`
|
|
);
|
|
});
|
|
|
|
test("transform context should have addIssue", () => {
|
|
const schema = z.transform((val, ctx) => {
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: "Not valid",
|
|
});
|
|
return val;
|
|
});
|
|
|
|
const result = schema.safeParse("test");
|
|
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.issues[0].message).toBe("Not valid");
|
|
}
|
|
});
|