Exploring Zod: A Comprehensive Guide to Powerful Data Validation in JavaScript/TypeScript

Raşit Çolakel
6 min readJun 11, 2023

--

What is Zod?

Created by Using Adobe Express

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

--

--