Zod
By Sarosh Nasir
TypeScript is already a great upgrade from JavaScript if you like types and want to ensure type-safety in your code base. Zod aids that goal by making sure that incoming data satisfies the purpose it is about to fulfill, among other features of course.
Zod is a TypeScript-first schema declaration and validation library with lots of features that I have yet to use. However, the basics are very handy and simple to get started with! For the purpose of introducing Zod, we’ll assume that we have a Node.js backend with an endpoint for adding a user to our system.
function addUserEndpoint(req: any, res: any) {
const user = req.body as User;
/* Checks, condictions, etc. */
addUserService(user);
}
function addUserService(user: User) {
/* Do something with user */
}
At this point, one might start extracting data from the payload, ex: payload.someField
, with or without checking if the value is undefined or null.
While using as User
helps dynamically map fields to the User
object, some might be undefined or maybe the payload isn’t in correct format.
There’s lots of things to consider before we can continue with the payload. Instead, let’s define a schema and infer a type from that schema.
How will this help? Through the help of Zod and its data validation capabilities!
Data Validation
Let’s say we have a user system that collects a users name, age and favorite tv shows.
import { z } from "zod";
const userSchema = z.object({
name: z.string(),
age: z.number(),
favoriteShows: z.array(z.string()),
});
The object schema above defines a user in our system.
z.object()
says that it’s an objectz.string()
defines the name asstring
z.number()
defines age asnumber
z.array(z.string())
defines favorite tv shows asstring[]
.
What now? Well, now we can create a type User
and even parse data to see if it fits our schema and therefore our User type.
.parse
// Extract the inferred type
type User = z.infer<typeof User>;
// Example of incoming data
const goodFoo = {
name: "Sarosh",
age: 29,
favoriteShows: ["Stranger Things", "Brooklyn 99"],
};
// Parsing of incoming data
const fooUser = userSchema.parse(foo);
// { name: "Sarosh", age: 29, favoriteShows: ["Stranger Things", "Brooklyn 99"]}
Luckily for us, the incoming data was valid. Let’s look at an erroneous example.
const badFoo = {
name: 1337,
age: "29",
favoriteShows: ["Stranger Things", "Brooklyn 99"],
};
const badUser = userSchema.parse(badFoo); // throws ZodError
/*
ZodError.issues:
[
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": [
"name"
],
"message": "Expected string, received number"
},
{
"code": "invalid_type",
"expected": "number",
"received": "string",
"path": [
"age"
],
"message": "Expected number, received string"
}
]
*/
If the data isn’t successfully parsed, Zod will throw a ZodError with clear information about what’s wrong with the parsed data.
For this case we can simple surround the .parse()
call in a try-catch
. zodError.issues
will list all the errors that Zod found.
.safeParse
For those that don’t like that approach, you can use the .safeParse()
approach.
safeParse
returns an object with information indicating whether the parsing was successful.
If it was successful, then you can extract the data, if not then you can extract the error.
For example, consider our two data objects goodFoo
and badFoo
.
userSchema.safeParse(goodFoo); // => { success: true, data: {...} }
userSchema.safeParse(badFoo); // => { success: false, error: ZodError }
const parsedData = userSchema.safeParse(data);
if (!parsedData.success {
const zodError = parsedData.error;
}
const user: User = parsedData.data;
In this case, we can use a simple if-statement
to check if the parse was successful.
Data Requirements
We’ve gone through the basics of Zod in regards to schemas and types, as well as how to extract the data. Now I want to focus on setting requirements for the incoming data. Let’s say that we want to make sure that the name field is not empty, the age is optional and there should be at most 3 favorite shows. All this can be defined in the schema.
const userSchema = z.object({
name: z.string().min(1),
age: z.number().optional(), // or z.optional(z.number())
favoriteShows: z.array(z.string()).max(3),
});
// Want to define an age limit?
const userSchema = z.object({
name: z.string().min(1),
age: z.optional(z.number().gte(18)),
favoriteShows: z.array(z.string()).max(3),
});
TL;DR
Schemas & Types
// Primitive data types
const someSchema = z.string();
// Objects
const simpleUserSchema = z.object({
name: z.string(),
age: z.number(),
favoriteShows: z.array(z.string()),
});
// Complex objects
const complexUserSchema = z.object({
name: z.string(),
age: z.number(),
address: z.object({
street: z.string(),
zip: z.number(),
city: z.string(),
}),
favoriteShows: z.array(z.string()),
});
type User = z.infer<typeof mySchema>;
Method using try-catch
import { z, ZodError } from "zod";
const userSchema = z.object({
name: z.string(),
age: z.number(),
favoriteShows: z.array(z.string()),
});
type User = z.infer<typeof mySchema>;
var myObj: User;
try {
myObj = userSchema.parse(data);
} catch (e) {
if (e instanceof ZodError) {
const zodError = e as ZodError;
console.error(zodError.issues);
} else {
const error = e as Error;
console.error(error.message);
}
}
Method using if-statement
import { z, ZodError } from "zod";
const userSchema = z.object({
name: z.string(),
age: z.number(),
favoriteShows: z.string().array(),
});
type User = z.infer<typeof userSchema>;
const parsedData = userSchema.safeParse(data);
if (!parsedData.success) {
const zodError = parsedData.error;
/* Error handling */
} else {
const user: User = parsedData.data;
/* Continue logic */
}
Check the requirements here and get started immediately!