ZOD - TypeScript-first schema data validation

I'm an aspiring frontend developer currently a 3rd college student at Indore. I am passionate about frontend web development, and UI/UX designing and recently I've taken a lot of interest in Opensource
Do you guys know that Typescript only does type-checking at compile time and no runtime checks?
Well, why do we even need runtime checks after typescript compile time checks, well the first use could be validating data on the server side, which typescript has no idea of at compile time.
we can't always be sure about what type of variables we'll get from external sources like APIs or form inputs.
For example, when we have some form to be filled, Typescript can't guarantee the user-filled inputs to be 100% type correct as we want on our database.
What is Zod and why do we need it?
Zod is a tool that solves this problem. It fills this TypeScript blindspot and helps with type safety during runtime by having an additional runtime check.
Zod is a TypeScript-first schema declaration and validation library. It provides safety checks at runtime working in conjunction with Typescript!
Installation
Zod can be installed into your react application using npm or yarn
npm install zod
yarn add zod
Requirements
- TypeScript 4.5+!
strictmode must be enabled in yourtsconfig.jsonfile// tsconfig.json { "compilerOptions": { "strict": true } }
Usage
Let's string by creating a very simple string schema using zod
import { z } from "zod";
const stringSchema = z.string().min(10).max(15);
stringSchema.parse("example string")
This will be safely parsed as it follows the zod schema we used to parse it.
But if we try something that outflows the schema parsing instructions, such as
stringSchema.parse("string of much greater length")
This will throw an error
{
code: 'too_big',
maximum: 15,
type: 'string',
inclusive: true,
message: 'String must contain at most 15 characters,
path: []
}
Zod also provides various other methods such as
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
z.symbol();
// empty types
z.undefined();
z.null();
z.void(); // accepts undefined
// catch-all types
// allows any value
z.any();
z.unknown();
// never type
// allows no values
z.never();
Objects in zod
Objects can be used extensively whether they be validating a user form or fetching data from an API
Let's take an example for validating a user form data
const userData = z.object({
firstName: z.string().min(1).max(10),
lastName: z.string().min(1).max(10),
email: z.string().email(),
})
zod to parse API data
In this example let's fetch data from the random user API and validate it using zod
const randomUsers = async () => {
const data = await fetch("https://swapi.dev/api/people/")
.then((res) => res.json(),
);
};
Now this would return an array of users, we can parse the result using the z.array() method
const randomUsers = z.object({
results: z.array(z.object()),
});
const parsedData = randomUsers.parse(data);
return parsedData.results;
Here we are declaring that the results array could be of any object type, we can further specify the data inside the result array to be of the user type like this
const randomUser = z.object({
name: z.object({
first: z.string(),
last: z.string()
})
})
// declare the type of results array to be randomUser
const randomUsers = z.object({
results: z.array(randomUser),
});
const parsedData = randomUsers.parse(data);
return parsedData.results;
Make schema values optional
What if we want our email input to be optional here, we can do it as such by adding .optional() to the end of our email schema:
const userData = z.object({
firstName: z.string().min(1).max(10),
lastName: z.string().min(1).max(10),
email: z.string().email().optional(),
})
Default value in the schema
What if we want email to be optional but don't want it to be undefined in our database, we can do so by passing a default value in its place by adding .default() to the end of our email schema:
const userData = z.object({
firstName: z.string().min(1).max(10),
lastName: z.string().min(1).max(10),
email: z.string().email().default("default@gmail.com"),
})
Reducing Duplicate values by composing schemas
Suppose we have this schema with User, Post, Comment
const User = z.object({
id: z.string().uuid(),
name: z.string(),
});
const Post = z.object({
id: z.string().uuid(),
title: z.string(),
});
const Comment = z.object({
id: z.string().uuid(),
comment: z.string(),
});
Notice that id is repeated in every schema
A very basic solution could be placing id in its own type and other schemas can use that reference.
const Id = z.string().uuid();
const User = z.object({
id: Id,
name: z.string(),
});
const Post = z.object({
id: Id,
title: z.string(),
body: z.string(),
});
const Comment = z.object({
id: Id,
text: z.string(),
});
But this is still quite repetitive, Another solution can be by using the extend method.
We can create a base object schema with id and then extend all other schema and add to the base schema as such:
const schemaWithId = z.object({
id: z.string().uuid()
});
const User = ObjectWithId.extend({
name: z.string(),
});
const Post = ObjectWithId.extend({
title: z.string(),
});
const Comment = ObjectWithId.extend({
comment: z.string(),
});
now we can add multiple values directly to share between all the objects
const schemaWithId = z.object({
id: z.string().uuid(),
createdAt: z.datetime()
});
Both id and createdAt values will be shared between all the objects.
Transform Data within the schema
Let's take back the random user but with a slight change that we are receiving fullName instead of separate firstName and lastName but and we want to split the fullName value into firstName and lastName
This can be done by transforming the fullName value by splitting it at the " " blank space within the schema by using the transform method
const userData = z.object({
fullName: z.string()
})
.transform(user => ({
...user,
nameAsArray: user.name.split( " " ),
}));
the transform method can be used in various ways to transform that data within the schema.
Type Inference
Suppose we already have defined our schema somewhere and want to use the same schema definition somewhere else without rewriting the schema again, we can do such by extracting its typescript type.
We can extract the TypeScript type of any schema with z.infer<typeof schema>
const user = z.object({
name: z.string(),
age: z.number(),
});
type userType = z.infer<typeof user>;
zod is a very easy library to implement and having both compile time and runtime checks will even boost your application's performance.
I hope these help you to get a basic understanding of how zod works. zod is much more than just this, to learn and understand much more about zod, use these official docs
Thanks for reading till the last
Reach me on Twitter



