Zod Validation - Runtime Typechecking
In this post you will learn what is Zod library and how it helps us to validate our data.
Here I'm on the official website and here we can see that Zod is "Typescript first schema validation with static type inference". Which actually means that we will use this library if we have some object and we want to validate it through specific rules.
For example you have a user with name, id and age. With Zod you can create a schema for this user with rules inside and you can just check your object if it is valid or not.
But the most interesting point for me is that Zod is not the first library which can do such stuff. We have previously similar libraries like for example Yup which were also quite popular.
import { object, string, number, date, InferType } from 'yup';
let userSchema = object({
name: string().required(),
age: number().required().positive().integer(),
email: string().email(),
website: string().url().nullable(),
createdOn: date().default(() => new Date()),
});
// parse and assert validity
const user = await userSchema.validate(await fetchUser());
type User = InferType<typeof userSchema>;
/* {
name: string;
age: number;
email?: string | undefined
website?: string | null | undefined
createdOn: Date
}*/
As you can see in Yup it was super similar to Zod. We define a schema which is just an object with properties and every property has some rules. And it also works inside Typescript just like Zod.
From my opinion a lot of people are talking about Zod library just because this library was promoted better than the others.
The last thing that I want to say before we start coding is that Zod is a library which will do validation in the runtime. Yes we are talking here about Typescript but the main idea is that we are calling it to check the data in the runtime.
Real usage
Let's look on the problem that we have. 2 most popular use cases where you will get runtime errors even if everything is covered with Typescript are forms where user type something or API calls because you have no idea what you will get from the API.
As you can see here I have a small project with the list of users inside the table. Most importantly we are getting these users from the API call. So we are getting back from API an array of objects with id, name and age.
It all looks fine but at some point it can break. Now I want to change the data that I'm getting from the API.
{
"users": [
{
"id": "1",
"name": "Jack",
"age": 25
},
{
"id": "2",
"name": "John",
"age": 20
},
{
"id": "3",
"name": "Mike",
"age": 30
},
{
"id": "4",
"name": "Peter",
"age": 25
},
{
"id": "5",
"name": null,
"age": 31
}
]
}
Here is our data from the backend and instead of the string I put null
inside the last object of the array.
As you can see in browser our page is completely broken because we tried to apply method to the null and couldn't work.
Which means even with the whole code covered with Typescript it is still error prone to runtime data.
Adding Zod
This is why we want to add Zod here to make sure that we get our users from the API in correct format.
npm install zod
First of all we installed Zod library.
Inside our application we have a UserInterface
which we don't need now because we will create Zod schema instead.
export interface UserInterface {
id: string;
name: string;
age: number;
}
The main problem with this interface is that it doesn't exist in runtime. It is only for Typescript.
// src/app/usersTable/types/user.model.ts
import { z } from 'zod';
export const UserSchema = z.object({
id: z.string(),
name: z.string(),
age: z.number(),
});
export type UserType = z.infer<typeof UserSchema>;
Here is how we created a Zod schema instead. By using z.object
we can define a schema with different fields inside. In our case we just define the data types but it is also possible to check length or use rules for email for example.
Additionally we exported UserType
from our schema by using z.infer
. It gives us exactly the same User interface like we defined previously. But now we can use both this type and our schema for runtime validation.
import { UserSchema, UserType } from '../types/user.model';
@Injectable()
export class UsersService {
constructor(private http: HttpClient) {}
getUsers(sorting: SortingInterface): Observable<UserType[]> {
const url = `http://localhost:3004/users?_sort=${sorting.column}&_order=${sorting.order}`;
return this.http.get<UserType[]>(url).pipe(
map((users) => {
return users.map((user) => ({
...user,
name: user.name.toUpperCase(),
}));
})
);
}
}
Here is our updated service to fetch users. The only change that I did is replacing UserInterface
and using UserType
instead. The whole logic stays the same.
getUsers(sorting: SortingInterface): Observable<UserType[]> {
const url = `http://localhost:3004/users?_sort=${sorting.column}&_order=${sorting.order}`;
return this.http.get<UserType[]>(url).pipe(
map((users) => users.map((user) => UserSchema.parse(user))),
map((users) => {
return users.map((user) => ({
...user,
name: user.name.toUpperCase(),
}));
})
);
}
But now we added an additional map with UserSchema.parse
inside. This line just validates every single user in the array against our schema. If it is valid it simply returns an object but if not it will throw the error.
As you can see in browser we directly get an error inside console and we know where the problem is. We see that we got null
instead of the string
. If we change our response to the valid one our application will work as before.
Working with API can be really difficult especially if this API is implemented by another team. In this case Zod helps you to be on the safe side and you directly see where the problem is.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done