Zod
Created and maintained by @vriad. The motivation for this library and a detailed comparison to various alternatives can be found at https://vriad.com/blog/zod.
If you find this package useful, leave a star to help more folks find it āļøš¤
Table of contents
Installation
To install the latest version:
npm install --save zod
yarn add zod
TypeScript versions
Zod 1.0.x is compatible with TypeScript 3.2+.
Usage
Zod is a validation library designed for optimal developer experience. It's a TypeScript-first schema declaration library with rigorous (and correct!) inferred types, incredible developer experience, and a few killer features missing from the existing libraries.
- It takes advantage of TypeScript generic inference to statically infer the types of your schemas, eliminating the need to define static types and runtime validators separately.
- Eliminates the need to keep static types and runtime validators in sync by hand
- It has a composable, declarative API that makes it easy to define complex types concisely.
- Schemas are immutable. All methods return a new schema instance.
Zod was also designed with some core principles designed to make all declarations as non-magical and developer-friendly as possible:
- All fields are required unless explicitly marked as optional (just like TypeScript!)
- Schemas are immutable; methods (i.e.
.optional()
return a new instance. - Zod schemas operate on a "Parse, don't validate!" basis!
Primitives
import * as z from 'zod';
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
const undefinedSchema = z.undefined();
const nullTypeSchema = z.null();
Literals
const tuna = z.literal('tuna');
const twelve = z.literal(12);
const tru = z.literal(true);
Currently there is no support for Date literals in Zod. If you have a use case for this feature, please file an issue.
Parsing and validation
Given a Zod schema, you can call its .parse(data)
method to check data
is valid. If it is, a value is returned with full type information! Otherwise, an error is thrown.
IMPORTANT: The variable you pass into .parse
is not the same variable you get back out. Assuming the value passes validation, you get back a deep clone of the object you passed in.
const stringSchema = z.string();
stringSchema.parse('fish');
stringSchema.parse(12);
You can also use a Zod schema as a type guard using the schema's .check()
method, like so:
const stringSchema = z.string();
const blob: any = 'Albuquerque';
if (stringSchema.check(blob)) {
}
You can use the same method to check for invalid data:
const stringSchema = z.string();
const process = (blob: any) => {
if (!stringSchema.check(blob)) {
throw new Error('Not a string');
}
};
To learn more about error handling with Zod, jump to Errors.
Type inference
You can extract the TypeScript type of any schema with z.infer<typeof [schema]>
.
const A = z.string();
type A = z.infer<typeof A>;
const u: A = 12;
const u: A = 'asdf';
We'll include examples of inferred types throughout the rest of the documentation.
Objects
const dogSchema = z.object({
name: z.string(),
neutered: z.boolean(),
});
type Dog = z.infer<typeof dogSchema>;
const cujo = dogSchema.parse({
name: 'Cujo',
neutered: true,
});
const fido: Dog = {
name: 'Fido',
};
.shape
property
const Location = z.shape({ latitude: z.number(), longitude: z.number() });
const Business = z.shape({
location: Location,
});
Business.shape.location;
Merging
You can combine two object schemas with .merge
, like so:
const BaseTeacher = z.object({ subjects: z.array(z.string()) });
const HasID = z.object({ id: z.string() });
const Teacher = BaseTeacher.merge(HasId);
type Teacher = z.infer<typeof Teacher>;
You're able to fluently chain together many .merge
calls as well:
const Teacher = BaseTeacher.merge(HasId)
.merge(HasName)
.merge(HasAddress);
.merge
is just syntactic sugar over the more generic z.intersection
which is documented below.
IMPORTANT: the schema returned by .merge
is the intersection of the two schemas. The schema passed into .merge
does not "overwrite" properties of the original schema. To demonstrate:
const Obj1 = z.object({ field: z.string() });
const Obj2 = z.object({ field: z.number() });
const Merged = Obj1.merge(Obj2);
type Merged = z.infer<typeof merged>;
To "overwrite" existing keys, use .augment
(documented below).
Augmentation
You can augment an object schema with the .augment
method.
const Animal = z
.object({
species: z.string(),
})
.augment({
population: z.number(),
});
ā ļø You can use .augment
to overwrite fields! Be careful with this power!
const ModifiedAnimal = Animal.augment({
species: z.array(z.string()),
});
Masking
Object masking is one of Zod's killer features. It lets you create slight variations of your object schemas easily and succinctly.
const Recipe = z.object({
id: z.string(),
name: z.string(),
ingredients: z.array(z.string()),
});
Inspired by TypeScript's built-in Pick
and Omit
utility types, all Zod object schemas have .pick
and .omit
methods that return a "masked" version of the schema.
To only keep certain keys, use .pick
.
const JustTheName = Recipe.pick({ name: true });
type JustTheName = z.infer<typeof JustTheName>;
To remove certain keys, use .omit
.
const NoIDRecipe = Recipe.omit({ id: true });
type NoIDRecipe = z.infer<typeof NoIDRecipe>;
This is useful for database logic, where endpoints often accept as input slightly modified versions of your database schemas. For instance, the input to a hypothetical createRecipe
endpoint would accept the NoIDRecipe
type, since the ID will be generated by your database automatically.
This is a vital feature for implementing typesafe backend logic, yet as far as I know, no other validation library (yup, Joi, io-ts, runtypes, class-validator, ow...) offers similar functionality as of this writing (April 2020). This is one of the must-have features that inspired the creation of Zod.
Partials
Inspired by the built-in TypeScript utility type Partial, all Zod object schemas have a .partial
method that makes all properties optional.
Starting from this object:
const user = z.object({
username: z.string(),
location: z.object({
latitude: z.number(),
longitude: z.number(),
}),
});
We can create a partial version:
const partialUser = user.partial();
const partialUser = z.object({
username: user.shape.username.optional(),
location: user.shape.location.optional(),
});
Or you can use .deepPartial
:
const deepPartialUser = user.deepPartial();
Important limitation: deep partials only work as expected in hierarchies of object schemas. It also can't be used on recursive schemas currently, since creating a recursive schema requires casting to the generic ZodType
type (which doesn't include all the methods of the ZodObject
class). Currently an improved version of Zod is under development that will have better support for recursive schemas.
Unknown keys
IMPORTANT: By default, Zod object schemas do not allow unknown keys.
dogSchema.parse({
name: 'Spot',
neutered: true,
color: 'brown',
});
This is an intentional decision to make Zod's behavior consistent with TypeScript. Consider this:
const spot: Dog = {
name: 'Spot',
neutered: true,
color: 'brown',
};
TypeScript doesn't allow unknown keys when assigning to an object type, so neither does Zod (by default). If you want to allow this, just call the .nonstrict()
method on any object schema:
const dogSchemaNonstrict = dogSchema.nonstrict();
dogSchemaNonstrict.parse({
name: 'Spot',
neutered: true,
color: 'brown',
});
This change is reflected in the inferred type as well:
type NonstrictDog = z.infer<typeof dogSchemaNonstrict>;
Records
Records are similar to object schemas, but don't enforce a type restriction on the keys. For instance:
const objectSchema = z.object({ name: z.string() });
objectSchema
only accepts objects with single key: name
. You could use .nonstrict()
to create a schema that accepts unknown keys, but that schema won't validated the values associated with those unknown keys.
const nonstrict = objectSchema.nonstrict();
type nonstrict = z.infer<typeof nonstrict>;
const parsed = nonstrict.parse({ name: 'Serena', bar: ['whatever'] });
parsed.bar;
But what if you want an object that enforces a schema on all of the values it contains? That's when you would use a record.
const User = z.object({
name: z.string(),
});
const UserStore = z.record(User);
type UserStore = z.infer<typeof UserStore>;
This is particularly useful for storing or caching items by ID.
const userStore: UserStore = {};
userStore['77d2586b-9e8e-4ecf-8b21-ea7e0530eadd'] = {
name: 'Carlotta',
};
userStore['77d2586b-9e8e-4ecf-8b21-ea7e0530eadd'] = {
whatever: 'Ice cream sundae',
};
And of course you can call .parse
just like any other Zod schema.
UserStore.parse({
user_1328741234: { name: 'James' },
});
A note on numerical keys
You may have expected z.record()
to accept two arguments, one for the keys and one for the values. After all, TypeScript's built-in Record type does (Record<KeyType, ValueType>
)Otherwise, how do you represent the TypeScript type Record<number, any>
in Zod?
As it turns out, TypeScript's behavior surrounding [k: number]
is a little unintuitive:
const testMap: { [k: number]: string } = {
1: 'one',
};
for (const key in testMap) {
console.log(`${key}: ${typeof key}`);
}
As you can see, JavaScript automatically casts all object keys to strings under the hood. Since Zod is trying to bridge the gap between static and runtime types, it doesn't make sense to provide a way of creating a record schema with numerical keys, since there's no such thing as a numerical key in runtime JavaScript.
Arrays
const dogsList = z.array(dogSchema);
dogsList.parse([{ name: 'Fido', age: 4, neutered: true }]);
dogsList.parse([]);
Non-empty lists
const nonEmptyDogsList = z.array(dogSchema).nonempty();
nonEmptyDogsList.parse([]);
Unions
Zod includes a built-in z.union
method for composing "OR" types.
const stringOrNumber = z.union([z.string(), z.number()]);
stringOrNumber.parse('foo');
stringOrNumber.parse(14);
Optional types
Unions are the basis for defining optional schemas. An "optional string" is just the union of string
and undefined
.
const A = z.union([z.string(), z.undefined()]);
A.parse(undefined);
type A = z.infer<typeof A>;
Zod provides a shorthand way to make any schema optional:
const B = z.string().optional();
const C = z.object({
username: z.string().optional(),
});
type C = z.infer<typeof C>;
Nullable types
Similarly, you can create nullable types like so:
const D = z.union([z.string(), z.null()]);
Or you can use the shorthand .nullable()
:
const E = z.string().nullable();
type E = z.infer<typeof D>;
You can create unions of any two or more schemas.
const F = z
.union([z.string(), z.number(), z.boolean()])
.optional()
.nullable();
F.parse('tuna');
F.parse(42);
F.parse(true);
F.parse(undefined);
F.parse(null);
F.parse({});
type F = z.infer<typeof F>;
Enums
You can combine unions and string literals to create an enum schemas.
const FishEnum = z.union([z.literal('Salmon'), z.literal('Tuna'), z.literal('Trout')]);
FishEnum.parse('Salmon');
FishEnum.parse('Flounder');
You can also use the built-in z.enum()
function, like so:
const FishEnum = z.enum(['Salmon', 'Tuna', 'Trout']);
FishEnum.Values.Salmon;
FishEnum.Values;
Intersections
Intersections are useful for creating "logical AND" types.
const a = z.union([z.number(), z.string()]);
const b = z.union([z.number(), z.boolean()]);
const c = z.intersection(a, b);
type c = z.infer<typeof C>;
const stringAndNumber = z.intersection(z.string(), z.number());
type Never = z.infer<typeof stringAndNumber>;
This is particularly useful for defining "schema mixins" that you can apply to multiple schemas.
const HasId = z.object({
id: z.string(),
});
const BaseTeacher = z.object({
name: z.string(),
});
const Teacher = z.intersection(BaseTeacher, HasId);
type Teacher = z.infer<typeof Teacher>;
Tuples
These differ from arrays in that they have a fixed number of elements, and each element can have a different type.
const athleteSchema = z.tuple([
z.string(),
z.number(),
z.object({
pointsScored: z.number(),
}),
]);
type Athlete = z.infer<typeof athleteSchema>;
Recursive types
You can define a recursive schema in Zod, but because of a limitation of TypeScript, their type can't be statically inferred. If you need a recursive Zod schema you'll need to define the type definition manually, and provide it to Zod as a "type hint".
interface Category {
name: string;
subcategories: Category[];
}
const Category: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(Category),
}),
);
Category.parse({
name: 'People',
subcategories: [
{
name: 'Politicians',
subcategories: [{ name: 'Presidents', subcategories: [] }],
},
],
});
Unfortunately this code is a bit duplicative, since you're declaring the types twice: once in the interface and again in the Zod definition. If your schema has lots of primitive fields, there's a way of reducing the amount of duplication:
const BaseCategory = z.object({
name: z.string(),
tags: z.array(z.string()),
itemCount: z.number(),
});
interface Category extends z.infer<typeof BaseCategory> {
subcategories: Category[];
}
const Category: z.ZodType<Category> = BaseCategory.merge(
z.object({
subcategories: z.lazy(() => z.array(Category)),
}),
);
Cyclical objects
Validation still works as expected even when there are cycles in the data.
const cyclicalCategory: any = {
name: 'Category A',
};
cyclicalCategory.subcategories = [cyclicalCategory];
const parsedCategory = Category.parse(cyclicalCategory);
parsedCategory.subcategories[0].subcategories[0].subcategories[0];
Function schemas
Zod also lets you define "function schemas". This makes it easy to validate the inputs and outputs of a function without intermixing your validation code and "business logic".
You can create a function schema with z.function(args, returnType)
which accepts these arguments.
args: ZodTuple
The first argument is a tuple (created with z.tuple([...])
and defines the schema of the arguments to your function. If the function doesn't accept arguments, you can pass an empty tuple (z.tuple([])
).returnType: ZodType
The second argument is the function's return type. This can be any Zod schema.
const args = z.tuple([z.string()]);
const returnType = z.number();
const myFunction = z.function(args, returnType);
type myFunction = z.infer<typeof myFunction>;
z.function
actually returns a higher-order "function factory". Every "factory" has .implement()
method which accepts a function as input and returns a new function.
const myValidatedFunction = myFunction(x => {
return x.trim().length;
});
myValidatedFunction
now automatically validates both its inputs and return value against the schemas provided to z.function
. If either is invalid, the function throws.
This way you can confidently write application logic in a "validated function" without worrying about invalid inputs, scattering schema.validate()
calls in your endpoint definitions,or writing duplicative types for your functions.
Here's a more complex example showing how to write a typesafe API query endpoint:
const args = z.tuple([
z.object({ nameStartsWith: z.string() }),
z.object({ skip: z.number(), limit: z.number() }),
]);
const returnType = z.array(
z.object({
id: string(),
name: string(),
}),
);
const FetcherEndpoint = z.function(args, returnType);
const searchUsers = FetcherEndpoint.validate((filters, pagination) => {
filters.nameStartsWith;
filters.ageLessThan;
const users = User.findAll({
});
return 'salmon';
});
const users = searchUsers(
{
nameStartsWith: 'John',
},
{
skip: 0,
limit: 20,
},
);
This is particularly useful for defining HTTP or RPC endpoints that accept complex payloads that require validation. Moreover, you can define your endpoints once with Zod and share the code with both your client and server code to achieve end-to-end type safety.
Errors
Zod includes a custom Error
subclass called ZodError
. All validation errors thrown by Zod are instances of ZodError
.
A ZodError
instance has an errors
property of type
{
path: (string | number)[],
message: string
}[]
This array represents all errors Zod encounters when attempting to parse a value.
const person = z.object({
name: {
first: z.string(),
last: z.string(),
},
age: z.number(),
address: z.array(z.string()),
});
try {
person.parse({
name: { first: 'Dave', last: 42 },
age: 'threeve',
address: ['123 Maple Street', {}],
});
} catch (err) {
if (err instanceof ZodError) {
console.log(JSON.stringify(err.errors));
console.log(err.message);
} else {
}
}
Comparison
There are a handful of other widely-used validation libraries, but all of them have certain design limitations that make for a non-ideal developer experience.
Joi
https://github.com/hapijs/joi
Doesn't support static type inference. š
Yup
https://github.com/jquense/yup
Yup is a full-featured library that was implemented first in vanilla JS, with TypeScript typings added later.
Yup supports static type inference, but unfortunately the inferred types aren't actually correct. Currently, the yup package treats all object properties as optional by default:
const schema = yup.object({
asdf: yup.string(),
});
schema.validate({});
type SchemaType = yup.InferType<typeof schema>;
Yup also mis-infers the type of required arrays.
const numList = yup
.array()
.of(yup.string())
.required();
numList.validateSync([]);
type NumList = yup.InferType<typeof numList>;
These may sound like nitpicks. But it's not acceptable that an object that's assignable to the inferred TypeScript type can fail validation by the validator it was inferred from.
io-ts
https://github.com/gcanti/io-ts
io-ts is an excellent library by gcanti. The API of io-ts heavily inspired the design of Zod.
In our experience, io-ts prioritizes functional programming purity over developer experience in many cases. This is a valid and admirable design goal, but it makes io-ts particularly hard to integrate into an existing codebase with a more procedural or object-oriented bias.
For instance, consider how to define an object with optional properties in io-ts:
import * as t from 'io-ts';
const A = t.type({
foo: t.string,
});
const B = t.partial({
bar: t.number,
});
const C = t.intersection([A, B]);
type C = t.TypeOf<typeof C>;
You must define the required and optional props in separate object validators, pass the optionals through t.partial
(which marks all properties as optional), then combine them with t.intersection
.
Consider the equivalent in Zod:
const C = z.object({
foo: z.string(),
bar: z.string().optional(),
});
type C = z.infer<typeof C>;
This more declarative API makes schema definitions vastly more concise.
io-ts
also requires the use of gcanti's functional programming library fp-ts
to parse results and handle errors. This is another fantastic resource for developers looking to keep their codebase strictly functional. But depending on fp-ts
necessarily comes with a lot of intellectual overhead; a developer has to be familiar with functional programming concepts, fp-ts
's nomenclature, and the Either
monad to do a simple schema validation. It's just not worth it for many people.
Runtypes
https://github.com/pelotom/runtypes
Changelog
zod version | release notes |
---|
zod@1.2.5 | .partial and .deepPartial on object schemas |
zod@1.2.3 | Date schemas |
zod@1.2.0 | .pick , .omit , and .augment on object schemas |
zod@1.1.0 | Records |
zod@1.0.11 | .nonstrict |
zod@1.0.10 | Type assertions with .check |
zod@1.0.4 | Empty tuples |
zod@1.0.0 | Type assertions, literals, enums, detailed error reporting |
zod@1.0.0 | Initial release |