Exploring Zod: A Comprehensive Guide to Powerful Data Validation in JavaScript/TypeScript
What is Zod?
Zod is a TypeScript-first schema validation library with static type inference. It is built with the following goals:
- TypeScript-first. Zod is built with TypeScript, for TypeScript. This means that you don’t need to learn a new syntax to describe your data. Zod schemas are just TypeScript types.
- Type inference. Zod tries to infer as much as possible, so you don’t have to write everything out. For example, if you pass a string to
z.number()
, Zod will infer that you want a schema that validates numbers, not strings. - Tiny bundle size. Zod is tiny. It has no dependencies and is less than 4kb minified and gzipped.
- Browser and Node.js support. Zod works in both Node.js and the browser.
- Great error messages. Zod has great error messages out of the box. It also has a powerful error API that allows you to customize error messages to your liking.
Installation
npm install zod
Zod Utilities
Zod has a number of utility functions that can be used to create schemas:
zod.string()
Creates a schema that validates strings.
Example
const name = z.string();
const data = name.safeParse("John"); // Success ✅
const data = name.safeParse(42); // Error ❌zod.number()
Creates a schema that validates numbers.
Example
const age = z.number();
const data = age.safeParse(42); // Success ✅
const data = age.safeParse("John"); // Error ❌
zod.boolean()
Creates a schema that validates booleans.
Example
const isHappy = z.boolean();
const data = isHappy.safeParse(true); // Success ✅
const data = isHappy.safeParse("John"); // Error ❌
zod.array()
Creates a schema that validates arrays.
Example
const names = z.array(z.string());
const data = names.safeParse(["John", "Jane"]); // Success ✅
const data = names.safeParse([42]); // Error ❌
zod.undefined()
Creates a schema that validates undefined.
Example
const itsHappy = z.undefined();
const data = isHappy.safeParse(undefined); // Success ✅
const data = isHappy.safeParse("John"); // Error ❌
zod.preprocess()
Creates a schema that preprocesses data before validating it. Sometimes you need to preprocess data before validating it. For example, you might want to trim whitespace from a string before validating it. You can do this with zod.preprocess()
. Or, you use zod the validate API request bodies, you can use it for boolean or number values. For example, you might want to convert the string "true" to the boolean true before validating it.
Example
Trimming whitespace from a string:
const name = z.string().preprocess((val) => val.trim());
const data = name.safeParse("John "); // Success ✅
const data = name.safeParse("John"); // Success ✅
const data = name.safeParse(42); // Error ❌
Converting the string to a boolean:
const isHappy = z.boolean().preprocess(Boolean, z.boolean());
const data = isHappy.safeParse("true"); // Success ✅
const data = isHappy.safeParse("false"); // Success ✅
const data = isHappy.safeParse("John"); // Error ❌
Convert the string to a number:
const age = z.number().preprocess(Number, z.number());
const data = age.safeParse("42"); // Success ✅
const data = age.safeParse("John"); // Error ❌
Converting the array of strings to an array of numbers:
const ages = z.array(z.string())
.preprocess(
(val) => val.map(Number),
z.array(z.number()));
const data = ages.safeParse(["42", "43"]); // Success ✅
const data = ages.safeParse(["John"]); // Error ❌
zod.optional()
Creates a schema that makes a value optional. This means that the value can be either the type of the schema or undefined
.
Example
const person = z.object({
name: z.string().optional(),
});
const data = person.safeParse({
name: "John",
}); // Success ✅
const data = person.safeParse({}); // Success ✅
zod.object()
Creates a schema that validates objects.
Example
const person = z.object({
name: z.string(),
age: z.number(),
});
const data = person.safeParse({
name: "John",
age: 42,
}); // Success ✅
const data = person.safeParse({
name: "John",
age: "42",
}); // Error ❌
zod.union()
Creates a schema that validates a union of multiple schemas.
Example
const name = z.union([z.string(), z.number()]);
const data = name.safeParse("John"); // Success ✅
const data = name.safeParse(42); // Success ✅
Inferring the type of a schema
Zod schemas are just TypeScript types. This means that you can use them to infer the type of a variable. For example, if you have a schema that validates a string, you can use it to infer the type of a variable:
const name = z.string();
type Name = z.infer<typeof name>;
const data: Name = "John"; // Success ✅
const data: Name = 42; // Error ❌
Zod Schema Methods
Zod schemas have a number of methods that can be used to validate data:
safeParse()
Validates data and returns a ZodSafeParseOutput
object. This is the recommended way to validate data.
Example
const name = z.string();
const data = name.safeParse("John"); // Success ✅
const data = name.safeParse(42); // Error ❌
parse()
Validates data and returns the parsed data. If the data is invalid, it throws an error. It is recommended to use safeParse()
instead of parse()
.
Example
const name = z.string();
const data = name.parse("John"); // Success ✅
const data = name.parse(42); // Error ❌
.merge()
Merges two schemas together.
Example
const person = z.object({
id: z.string(),
name: z.string(),
});
const age = z.object({
age: z.number(),
});
const personWithAge = person.merge(age);
type PersonWithAge = z.infer<typeof personWithAge>; // {id: string, name: string, age: number}
const data = personWithAge.safeParse({
id: "1",
name: "John",
age: 42,
}); // Success ✅
const data = personWithAge.safeParse({
id: "1",
name: "John",
age: "42",
}); // Error ❌
pick()
pick()
can be used to pick properties from an object.
Example
const person = z.object({
id: z.string(),
name: z.string(),
});
const personWithName = person.pick({name: true});
type PersonWithName = z.infer<typeof personWithName>; // {name: string}
const data = personWithName.safeParse({
name: "John",
}); // Success ✅
const data = personWithName.safeParse({
id: "1",
name: "John",
}); // Error ❌
omit()
omit()
can be used to omit properties from an object.
Example
const person = z.object({
id: z.string(),
name: z.string(),
});
const personWithoutName = person.omit({name: true});
type PersonWithoutName = z.infer<typeof personWithoutName>; // {id: string}
const data = personWithoutName.safeParse({
id: "1",
}); // Success ✅
const data = personWithoutName.safeParse({
id: "1",
name: "John",
}); // Error ❌
partial()
partial()
makes all properties optional.
Example
const person = z.object({
id: z.string(),
name: z.string(),
});
const partialPerson = person.partial();
type PartialPerson = z.infer<typeof partialPerson>; // {id?: string, name?: string}
const data = partialPerson.safeParse({
id: "1",
}); // Success ✅
const data = partialPerson.safeParse({
name: "John",
}); // Success ✅
const data = partialPerson.safeParse({
id: "1",
name: "John",
}); // Success ✅
deepPartial()
Like partial()
, but it makes all properties optional recursively.
Example
const person = z.object({
id: z.string(),
name: z.string(),
address: z.object({
street: z.string(),
city: z.string(),
}),
});
const partialPerson = person.deepPartial();
type PartialPerson = z.infer<typeof partialPerson>; // {id?: string, name?: string, address?: {street?: string, city?: string}}
const data = partialPerson.safeParse({
id: "1",
}); // Success ✅
const data = partialPerson.safeParse({
name: "John",
}); // Success ✅
const data = partialPerson.safeParse({
id: "1",
address: {
street: "Main Street",
},
}); // Success ✅
required()
Unlike partial()
, required()
makes all properties required.
Example
const person = z.object({
id: z.string().optional(),
name: z.string().optional(),
});
type Person = z.infer<typeof person>; // {id?: string, name?: string}
const requiredPerson = person.required();
type RequiredPerson = z.infer<typeof requiredPerson>; // {id: string, name: string}
const data = requiredPerson.safeParse({
id: "1",
}); // Error ❌
const data = requiredPerson.safeParse({
name: "John",
}); // Error ❌
const data = requiredPerson.safeParse({
id: "1",
name: "John",
}); // Success ✅
extend()
extend()
can be used to extend an object with additional properties.
Example
const person = z.object({
id: z.string(),
name: z.string(),
});
const personWithAge = person.extend({
age: z.number(),
});
type PersonWithAge = z.infer<typeof personWithAge>; // {id: string, name: string, age: number}
const data = personWithAge.safeParse({
id: "1",
name: "John",
age: 42,
}); // Success ✅
refine()
Creates a schema that refines data. It means that it validates data and then runs a function on it. If the function returns true
, the data is valid. If the function returns false
, the data is invalid.
Example
const person = z.object({
name: z.string(),
});
const personWithName = person.refine((val) => val.name === "John");
const data = personWithName.safeParse({
name: "John",
}); // Success ✅
const data = personWithName.safeParse({
name: "Jane",
}); // Error ❌
keyOf()
Creates a schema that validates a key of an object.
Example
const person = z.object({
name: z.string(),
});
const personWithName = person.keyOf();
type PersonWithName = z.infer<typeof personWithName>; // "name"
const data = personWithName.safeParse("name"); // Success ✅
const data = personWithName.safeParse("age"); // Error ❌
Conclusion
As you can see, Zod is a powerful schema validation library that is easy to use and has great error messages. It is also very fast and has a tiny bundle size. It is a great alternative to Joi and Yup.
For more information, see the Zod documentation