Zod: Schema Declaration and Validation in TypeScript
Zod is a TypeScript-first schema declaration and validation library. Zod tries to address TypeScript blindspot. TypeScript only does static type checking at compile time and doesn’t have any runtime checks at all. Zod lets us work around this by checking types at the runtime level as well as the type level. With Zod, you can validate at both runtime and compile time.
Why Zod?
Zod has several advantages over other schema validation libraries such as Yup, Joi, and io-ts. For example, Zod is very flexible and can chain several instances together, thereby preventing duplication of static types. It has no dependencies which means that it can be installed and used without any other libraries. This helps keep bundle size smaller and makes it a good choice for projects that need to support a wide range of platforms. Additionally, Zod works well with TypeScript since it is designed to be TypeScript-first. The library will automatically infer the static TypeScript type for data structures so there's no need to declare types twice - once in Zod and again in TypeScript.
Zod can be used for various purposes such as validating HTTP response bodies or validating JSON Schema against actual data. Zod is also better for validating data passed between client and server.
Primitives in Zod
In Zod, primitives are the basic data types that can be validated using a schema. These include string, number, boolean, null, and undefined. You can create a Zod schema for any TypeScript primitive using the corresponding method provided by Zod. For example, z.string()
creates a schema that validates strings, and z.number()
creates a schema that validates numbers. This example shows string validation:
import { z } from "zod";
const dataInput = z.string().min(8).max(20);
dataInput.parse("A long text");
dataInput.parse("AbC"); // error
Zod includes a handful of string-specific and number-specific validations.
z.number().gt(10);
z.number().gte(10); // alias .min(5)
z.number().lt(10);
z.number().lte(10); // alias .max(5)
z.number().int(); // value must be an integer
z.string().email();
z.string().url();
z.string().emoji();
z.string().uuid();
z.string().cuid();
z.string().cuid2();
z.string().regex(regex);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().trim(); // trim whitespace
z.string().datetime(); // defaults to UTC
z.string().ip();
Objects in Zod
Zod provides primitives for validating objects with specific properties and their types. You can create a Zod schema for an object using the z.object()
method. Here's an example of how to validate an address object using Zod in TypeScript:
import * as z from 'zod';
const addressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string().length(2),
zip: z.string().regex(/^\d{5}$/),
});
type Address = z.infer<typeof addressSchema>;
function validateAddress(address: Address): void {
try {
addressSchema.parse(address);
console.log('Address is valid');
} catch (error) {
console.error('Address is invalid:', error.message);
}
}
const validAddress: Address = {
street: '123 Main St',
city: 'Sometown',
state: 'CA',
zip: '34578',
};
const invalidAddress: Address = {
street: '',
city: '',
state: 'California',
zip: '1234',
};
validateAddress(validAddress); // prints "Address is valid"
validateAddress(invalidAddress); // prints "Address is invalid" with details about the validation failure
This code defines an addressSchema
object using the z.object()
function provided by Zod, which specifies the validation constraints for a valid address object.
The type Address = z.infer<typeof addressSchema>
line defines a type alias for the inferred type of the addressSchema
object, which is equivalent to { street: string, city: string, state: string, zip: string }
.
The validateAddress
function takes an address
parameter of type Address
, and attempts to parse it using the addressSchema.parse()
method. If parsing succeeds, it logs a message indicating that the address is valid. If parsing fails due to a validation error, it logs an error message with details about the validation failure.
The code then demonstrates how to use the validateAddress
function to validate two sample address objects (validAddress
and invalidAddress
). The first call to validateAddress()
succeeds and logs a message indicating that the address is valid. The second call to validateAddress()
fails due to some of the properties not meeting the validation constraints and logs an error message with details about the validation failure.
Complex schema objects
To write a complex schema validation in Zod, you can use the z.object()
method to define an object schema with specific properties and their types. Then, you can use other methods like z.array()
, z.union()
, and z.optional()
to define arrays, optional properties, and combined schemas. For example, consider the following code:
import { z } from 'zod';
const addressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string(),
zip: z.string().min(5),
});
const userSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
address: addressSchema,
phoneNumbers: z.array(z.string().min(10)),
});
const adminSchema = userSchema.extend({
role: z.literal('admin'),
});
const userOrAdminSchema = z.union([userSchema, adminSchema]);
type UserOrAdmin = z.infer<typeof userOrAdminSchema>;
const user: UserOrAdmin = {
name: 'John Doe',
age: 30,
email: 'johndoe@example.com',
address: {
street: '123 Main St',
city: 'Sometown',
state:'CA',
zip:'34578',
},
phoneNumbers:[
'555-555-5555',
'666-666-6666'
],
};
const admin: UserOrAdmin = {
name:'Elena Smith',
age :40,
email:'janesmith@example.com',
address:{
street:'456 Second St',
city:'Othertown',
state:'NY',
zip:'67890'
},
phoneNumbers:[
'777-777-7777'
],
role:'admin'
};
In this example, addressSchema
is defined as an object schema with specific properties and their types. Then, userSchema
is defined as an object schema that includes the address
property with its own schema. The adminSchema
extends the userSchema
by adding a new property called role
. Finally, the userOrAdminSchema
is defined as a union of both schemas. The UserOrAdmin
type is then used to define variables with properties that match either schema.
Type inference in Zod
To write a type inference in Zod, you can use the infer
keyword with the typeof
operator to extract the inferred type from a Zod schema. For example, consider the following code:
import { z } from 'zod';
const userSchema = z.object({
name: z.string(),
age: z.number(),
});
const adminSchema = z.object({
name: z.string(),
role: z.literal('admin'),
});
const userOrAdminSchema = z.union([userSchema, adminSchema]);
type UserOrAdmin = z.infer<typeof userOrAdminSchema>;
const user: UserOrAdmin = {
name: 'John Doe',
age: 30,
};
const admin: UserOrAdmin = {
name: 'Jonathan Smith',
role: 'admin',
};
In this example, z.union([userSchema, adminSchema])
creates a schema that validates either a user
object or an admin
object. The UserOrAdmin
type is then used to define variables with properties that match either schema.
Customizing error messages
Zod allows you to customize some common error messages when creating a schema. For example, you can customize the error message for a required field in a number schema using the required_error
option:
const age = z.number({
required_error: "Age is required",
invalid_type_error: "Age must be a number",
invalid_age: "Age must be larger than 13",
});
This will override the default error message for a missing value in the number schema.
Zod's errorMap
method allows you to map specific errors to custom messages. This can be useful for providing more descriptive error messages or translating error messages into different languages. To use the errorMap
method, you can define a function that takes an error object and returns a custom message for each error path. For example, the following code creates a schema that validates an email address and maps the default error message to a custom message.
import { z } from 'zod';
const emailSchema = z.string().email().errorMap((error) => {
if (error.path === 'email') {
return { message: 'Please enter a valid email address' };
}
return { message: 'Invalid input' };
});
This schema can then be used to validate input data during runtime. If the input data does not match the schema, an error will be thrown with the custom error message.
Conclusion
In conclusion, Zod provides several benefits such as static type inference for data structures, flexibility in schema design, minimal code requirements for validation, prevention of garbage values from being sent to databases, and developer-friendly error handling mechanisms that avoid common human errors in validators.