UNPKG

zod

Version:

TypeScript-first schema declaration and validation library with static type inference

583 lines (527 loc) • 12.6 kB
import { expect, expectTypeOf, test } from "vitest"; import { z } from "zod/v4"; test("recursion with z.lazy", () => { const data = { name: "I", subcategories: [ { name: "A", subcategories: [ { name: "1", subcategories: [ { name: "a", subcategories: [], }, ], }, ], }, ], }; const Category = z.object({ name: z.string(), get subcategories() { return z.array(Category).optional().nullable(); }, }); type Category = z.infer<typeof Category>; interface _Category { name: string; subcategories?: _Category[] | undefined | null; } expectTypeOf<Category>().toEqualTypeOf<_Category>(); Category.parse(data); }); test("recursion involving union type", () => { const data = { value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: null, }, }, }, }; const LL = z.object({ value: z.number(), get next() { return LL.nullable(); }, }); type LL = z.infer<typeof LL>; type _LL = { value: number; next: _LL | null; }; expectTypeOf<LL>().toEqualTypeOf<_LL>(); LL.parse(data); }); test("mutual recursion - native", () => { const Alazy = z.object({ val: z.number(), get b() { return Blazy; }, }); const Blazy = z.object({ val: z.number(), get a() { return Alazy.optional(); }, }); const testData = { val: 1, b: { val: 5, a: { val: 3, b: { val: 4, a: { val: 2, b: { val: 1, }, }, }, }, }, }; type Alazy = z.infer<typeof Alazy>; type Blazy = z.infer<typeof Blazy>; interface _Alazy { val: number; b: _Blazy; } interface _Blazy { val: number; a?: _Alazy | undefined; } expectTypeOf<Alazy>().toEqualTypeOf<_Alazy>(); expectTypeOf<Blazy>().toEqualTypeOf<_Blazy>(); Alazy.parse(testData); Blazy.parse(testData.b); expect(() => Alazy.parse({ val: "asdf" })).toThrow(); }); test("pick and omit with getter", () => { const Category = z.strictObject({ name: z.string(), get subcategories() { return z.array(Category); }, }); type Category = z.infer<typeof Category>; interface _Category { name: string; subcategories: _Category[]; } expectTypeOf<Category>().toEqualTypeOf<_Category>(); const PickedCategory = Category.pick({ name: true }); const OmittedCategory = Category.omit({ subcategories: true }); const picked = { name: "test" }; const omitted = { name: "test" }; PickedCategory.parse(picked); OmittedCategory.parse(omitted); expect(() => PickedCategory.parse({ name: "test", subcategories: [] })).toThrow(); expect(() => OmittedCategory.parse({ name: "test", subcategories: [] })).toThrow(); }); test("deferred self-recursion", () => { const Feature = z.object({ title: z.string(), get features(): z.ZodOptional<z.ZodArray<typeof Feature>> { return z.optional(z.array(Feature)); //.optional(); }, }); // type Feature = z.infer<typeof Feature>; const Output = z.object({ id: z.int(), //.nonnegative(), name: z.string(), get features(): z.ZodArray<typeof Feature> { return Feature.array(); }, }); type Output = z.output<typeof Output>; type _Feature = { title: string; features?: _Feature[] | undefined; }; type _Output = { id: number; name: string; features: _Feature[]; }; // expectTypeOf<Feature>().toEqualTypeOf<_Feature>(); expectTypeOf<Output>().toEqualTypeOf<_Output>(); }); test("deferred mutual recursion", () => { const Slot = z.object({ slotCode: z.string(), get blocks() { return z.array(Block); }, }); type Slot = z.infer<typeof Slot>; const Block = z.object({ blockCode: z.string(), get slots() { return z.array(Slot).optional(); }, }); type Block = z.infer<typeof Block>; const Page = z.object({ slots: z.array(Slot), }); type Page = z.infer<typeof Page>; type _Slot = { slotCode: string; blocks: _Block[]; }; type _Block = { blockCode: string; slots?: _Slot[] | undefined; }; type _Page = { slots: _Slot[]; }; expectTypeOf<Slot>().toEqualTypeOf<_Slot>(); expectTypeOf<Block>().toEqualTypeOf<_Block>(); expectTypeOf<Page>().toEqualTypeOf<_Page>(); }); test("mutual recursion with meta", () => { const A = z .object({ name: z.string(), get b() { return B; }, }) .readonly() .meta({ id: "A" }) .optional(); const B = z .object({ name: z.string(), get a() { return A; }, }) .readonly() .meta({ id: "B" }); type A = z.infer<typeof A>; type B = z.infer<typeof B>; type _A = | Readonly<{ name: string; b: _B; }> | undefined; // | undefined; type _B = Readonly<{ name: string; a?: _A; }>; expectTypeOf<A>().toEqualTypeOf<_A>(); expectTypeOf<B>().toEqualTypeOf<_B>(); }); test("intersection with recursive types", () => { const A = z.discriminatedUnion("type", [ z.object({ type: z.literal("CONTAINER"), }), z.object({ type: z.literal("SCREEN"), config: z.object({ x: z.number(), y: z.number() }), }), ]); // type A = z.infer<typeof A>; const B = z.object({ get children() { return z.array(C).optional(); }, }); // type B = z.infer<typeof B>; const C = z.intersection(A, B); type C = z.infer<typeof C>; type _C = ( | { type: "CONTAINER"; } | { type: "SCREEN"; config: { x: number; y: number; }; } ) & { children?: _C[] | undefined; }; expectTypeOf<C>().toEqualTypeOf<_C>(); }); test("object utilities with recursive types", () => { const NodeBase = z.object({ id: z.string(), name: z.string(), get children() { return z.array(Node).optional(); }, }); // Test extend const NodeOne = NodeBase.extend({ name: z.literal("nodeOne"), get children() { return z.array(Node); }, }); const NodeTwo = NodeBase.extend({ name: z.literal("nodeTwo"), get children() { return z.array(Node); }, }); // Test pick const PickedNode = NodeBase.pick({ id: true, name: true }); // Test omit const OmittedNode = NodeBase.omit({ children: true }); // Test merge const ExtraProps = { metadata: z.string(), get parent() { return Node.optional(); }, }; const MergedNode = NodeBase.extend(ExtraProps); // Test partial const PartialNode = NodeBase.partial(); const PartialMaskedNode = NodeBase.partial({ name: true }); // Test required (assuming NodeBase has optional fields) const OptionalNodeBase = z.object({ id: z.string().optional(), name: z.string().optional(), get children() { return z.array(Node).optional(); }, }); const RequiredNode = OptionalNodeBase.required(); const RequiredMaskedNode = OptionalNodeBase.required({ id: true }); const Node = z.union([ NodeOne, NodeTwo, PickedNode, OmittedNode, MergedNode, PartialNode, PartialMaskedNode, RequiredNode, RequiredMaskedNode, ]); }); test("tuple with recursive types", () => { const TaskListNodeSchema = z.strictObject({ type: z.literal("taskList"), get content() { return z.array(z.tuple([TaskListNodeSchema, z.union([TaskListNodeSchema])])).min(1); }, }); type TaskListNodeSchema = z.infer<typeof TaskListNodeSchema>; type _TaskListNodeSchema = { type: "taskList"; content: [_TaskListNodeSchema, _TaskListNodeSchema][]; }; expectTypeOf<TaskListNodeSchema>().toEqualTypeOf<_TaskListNodeSchema>(); }); test("recursion compatibility", () => { // array const A = z.object({ get array() { return A.array(); }, get optional() { return A.optional(); }, get nullable() { return A.nullable(); }, get nonoptional() { return A.nonoptional(); }, get readonly() { return A.readonly(); }, get describe() { return A.describe("A recursive type"); }, get meta() { return A.meta({ description: "A recursive type" }); }, get pipe() { return A.pipe(z.any()); }, get strict() { return A.strict(); }, get tuple() { return z.tuple([A, A]); }, get object() { return z .object({ subcategories: A, }) .strict() .loose(); }, get union() { return z.union([A, A]); }, get intersection() { return z.intersection(A, A); }, get record() { return z.record(z.string(), A); }, get map() { return z.map(z.string(), A); }, get set() { return z.set(A); }, get lazy() { return z.lazy(() => A); }, get promise() { return z.promise(A); }, }); }); test("recursive object with .check()", () => { const Category = z .object({ id: z.string(), name: z.string(), get subcategories() { return z.array(Category).optional(); }, }) .check((ctx) => { // Check for duplicate IDs among direct subcategories if (ctx.value.subcategories) { const siblingIds = new Set<string>(); ctx.value.subcategories.forEach((sub, index) => { if (siblingIds.has(sub.id)) { ctx.issues.push({ code: "custom", message: `Duplicate sibling ID found: ${sub.id}`, path: ["subcategories", index, "id"], input: ctx.value, }); } siblingIds.add(sub.id); }); } }); // Valid - siblings have unique IDs const validData = { id: "electronics", name: "Electronics", subcategories: [ { id: "computers", name: "Computers", subcategories: [ { id: "laptops", name: "Laptops" }, { id: "desktops", name: "Desktops" }, ], }, { id: "phones", name: "Phones", }, ], }; // Invalid - duplicate sibling IDs const invalidData = { id: "electronics", name: "Electronics", subcategories: [ { id: "computers", name: "Computers" }, { id: "phones", name: "Phones" }, { id: "computers", name: "Computers Again" }, // Duplicate at index 2 ], }; expect(() => Category.parse(validData)).not.toThrow(); expect(() => Category.parse(invalidData)).toThrow(); }); // biome-ignore lint: sadf export type RecursiveA = z.ZodUnion< [ z.ZodObject<{ a: z.ZodDefault<RecursiveA>; b: z.ZodPrefault<RecursiveA>; c: z.ZodNonOptional<RecursiveA>; d: z.ZodOptional<RecursiveA>; e: z.ZodNullable<RecursiveA>; g: z.ZodReadonly<RecursiveA>; h: z.ZodPipe<RecursiveA, z.ZodString>; i: z.ZodArray<RecursiveA>; j: z.ZodSet<RecursiveA>; k: z.ZodMap<RecursiveA, RecursiveA>; l: z.ZodRecord<z.ZodString, RecursiveA>; m: z.ZodUnion<[RecursiveA, RecursiveA]>; n: z.ZodIntersection<RecursiveA, RecursiveA>; o: z.ZodLazy<RecursiveA>; p: z.ZodPromise<RecursiveA>; q: z.ZodCatch<RecursiveA>; r: z.ZodSuccess<RecursiveA>; s: z.ZodTransform<RecursiveA, string>; t: z.ZodTuple<[RecursiveA, RecursiveA]>; u: z.ZodObject<{ a: RecursiveA; }>; }>, ] >; test("recursive type with `id` meta", () => { const AType = z.object({ type: z.literal("a"), name: z.string(), }); const BType = z.object({ type: z.literal("b"), name: z.string(), }); const CType = z.object({ type: z.literal("c"), name: z.string(), }); const Schema = z.object({ type: z.literal("special").meta({ description: "Type" }), config: z.object({ title: z.string().meta({ description: "Title" }), get elements() { return z.array(z.discriminatedUnion("type", [AType, BType, CType])).meta({ id: "SpecialElements", title: "SpecialElements", description: "Array of elements", }); }, }), }); Schema.parse({ type: "special", config: { title: "Special", elements: [ { type: "a", name: "John" }, { type: "b", name: "Jane" }, { type: "c", name: "Jim" }, ], }, }); });