Recursive schemas are schemas that refer to themselves. This makes them prone to circular reference problems. For example, Assume you are defining a tree structure. Each item in this type of data structure points to the next item recursively. The z.lazy method can be used in such cases to avoid circular reference issues.
Here is a set of scenarios where the z.lazy method is essential:
Without z.lazy defining such schemas would result in a circular dependency. TypeScript and other validation libraries cannot resolve such. This is why we need the z.lazy method.
z.lazy's signature accepts a callback function that returns a Zod schema. This structure allows for deferred evaluations. It means that the schema is evaluated when needed and not immediately.: Here's how to use it:
import { z } from 'zod';
const lazySchema = z.lazy(() => z.string());
lazySchema is defined as a schema that will ultimately validate a string. The use of z.lazy here is trivial but showcases how to set up the basic structure.
The main purpose of the callback is for deferred evaluation of the schema. Typescript would encounter circular references by evaluating it immediately.
Let's see a more powerful example that shows z.lazy being used to define recursive types.
const numberSchema = z.lazy(() => z.union([z.number(), z.string()]));
console.log(numberSchema.parse(42)); // Valid, outputs: 42
console.log(numberSchema.parse("hello")); // Valid, outputs: "hello"
console.log(numberSchema.parse(true)); // Invalid, throws an error
Above we see that anumberSchema defines a schema that can validate both a number and a string using z.union. This shows the flexible nature of lazy evaluation for handling more dynamic type
A recursive schema is all about object references itself, directly or indirectly. Its concept is most common in data structures e.g. trees, linked lists, and graphs. Zods z.lazy helps define such schemas while avoiding the circular reference issues.
Consider a tree. Each node has a value and may have a set of children where each child in turn is a tree node tool. Here is how z.lazy would be used in such a scenario:
import { z } from 'zod';
const TreeNodeSchema = z.lazy(() =>
z.object({
value: z.number(),
children: z.array(TreeNodeSchema), // Recursively references TreeNodeSchema
})
);
Let's understand what happened above:
Now let's see how we can use this schema to validate a tree-like data structure:
const exampleTree = {
value: 1,
children: [
{ value: 2, children: [] },
{ value: 3, children: [{ value: 4, children: [] }] },
],
};
const parsedTree = TreeNodeSchema.parse(exampleTree);
console.log(parsedTree);
In this example:
{
value: 1,
children: [
{ value: 2, children: [] },
{ value: 3, children: [{ value: 4, children: [] }] }
]
}
When dealing with recursive schemas you have to watch out for certain edge cases. If you don't you may run into validation errors and performance problems:
For this, we modify our code as follows:
const TreeNodeSchema = z.lazy(() =>
z.object({
value: z.number(),
children: z.array(TreeNodeSchema).default([]), // Default to an empty array
})
);
const MAX_DEPTH = 10;
const createTreeNodeSchema = (depth = 0): z.ZodType<any> =>
z.lazy(() =>
z.object({
value: z.number(),
children: depth < MAX_DEPTH ? z.array(createTreeNodeSchema(depth + 1)) : z.array(z.never()),
})
);
const LimitedDepthTreeNodeSchema = createTreeNodeSchema();
Such a setup sets a limit on allowed validation depth. This helps avoid infinite loops and StackOverflow issues.
Zods z.lazy integrates well with Zods error reporting. I provide detailed and nested error messages. This is important because handling errors in recursive schemas can get tricky. The error reporting is helpful during the debugging of complex structures. It assists in pinpointing exactly where the issue occurred.
Consider the following invalid tree structure:
const invalidTree = {
value: "not a number", // Invalid value
children: [
{ value: 5, children: [] },
{ value: 15, children: [{ value: 12, children: [] }] },
],
};
const invalidResult = TreeNodeSchema.safeParse(invalidTree);
if (!invalidResult.success) {
console.error("Validation failed:", invalidResult.error.errors);
}
In this case:
Validation failed: [
{
message: 'Expected number, received string',
path: ['value'],
code: 'invalid_type',
}
]
This output provides:
Let's see another example. This time we modify the invalid tree to include an error deeper in the hierarchy:
const deeplyInvalidTree = {
value: 1,
children: [
{ value: 5, children: [] },
{
value: 15,
children: [
{ value: "invalid", children: [] }, // Error at a nested level
],
},
],
};
const deepErrorResult = TreeNodeSchema.safeParse(deeplyInvalidTree);
if (!deepErrorResult.success) {
console.error("Validation failed:", deepErrorResult.error.errors);
}
Output:
Validation failed: [
{
message: 'Expected number, received string',
path: ['children', 1, 'children', 0, 'value'],
code: 'invalid_type',
}
]
In this example:
Zod also allows us to customize error messages. This can be useful to provide more meaningful feedback. We take advantage of the z.refine() method for this:
const CustomTreeNodeSchema = z.lazy(() =>
z.object({
value: z.number().or(z.nan()).refine((val) => val >= 0, {
message: "Node value must be a non-negative number",
}),
children: z
.array(TreeNodeSchema)
.refine((children) => children.length <= 10, {
message: "A node cannot have more than 10 children",
}),
})
);
const customErrorTree = {
value: -1, // Invalid: negative number
children: Array(12).fill({ value: 2, children: [] }), // Invalid: more than 10 children
};
const customErrorResult = CustomTreeNodeSchema.safeParse(customErrorTree);
if (!customErrorResult.success) {
console.error("Validation failed:", customErrorResult.error.errors);
}
Output
Validation failed: [
{
message: 'Node value must be a non-negative number',
path: ['value'],
code: 'custom_error',
},
{
message: 'A node cannot have more than 10 children',
path: ['children'],
code: 'custom_error',
}
]
In this customized example:
Consider a company structure where each employee can have subordinates. We can model this using z.lazy:
const EmployeeSchema = z.lazy(() =>
z.object({
name: z.string(),
position: z.string(),
subordinates: z.array(EmployeeSchema),
})
);
const companyStructure = {
name: "Alice",
position: "CEO",
subordinates: [
{
name: "Bob",
position: "CTO",
subordinates: [],
},
{
name: "Charlie",
position: "CFO",
subordinates: [
{
name: "David",
position: "Accountant",
subordinates: [],
},
],
},
],
};
const companyResult = EmployeeSchema.safeParse(companyStructure);
if (companyResult.success) {
console.log("Company structure is valid:", companyResult.data);
} else {
console.error("Company structure validation failed:", companyResult.error.errors);
}
You may receive complex JSON data from an API, and you need to ensure it adheres to a specific schema. Here’s how you can leverage the z.lazy method:
const ComplexSchema = z.lazy(() =>
z.object({
id: z.string(),
attributes: z.object({
name: z.string(),
age: z.number(),
children: z.array(ComplexSchema),
}),
})
);
const jsonData = {
id: "123",
attributes: {
name: "John Doe",
age: 30,
children: [
{
id: "124",
attributes: {
name: "Jane Doe",
age: 10,
children: [],
},
},
],
},
};
const jsonResult = ComplexSchema.safeParse(jsonData);
if (jsonResult.success) {
console.log("JSON data is valid:", jsonResult.data);
} else {
console.error("JSON validation failed:", jsonResult.error.errors);
}
The z.lazy method in Zod is a powerful feature that enables users to define recursive schemas easily. It simplifies the process of validating complex data structures. z.lazy method ensures robust data validation in applications.
simplify and inspire technology
©2024, basicutils.com