Zod Guide
Intro
Zod is a fantastic utility package by @colinhacks that allows for defining runtime schema validation and type-safety.
We use it extensively internally at Trigger.dev.
But there are a few places where we ask you to provide us with a Zod schema, for example when defining your own events:
client.defineJob({
id: "new-user",
name: "New user",
version: "0.1.0",
//the eventTrigger uses zod to define the schema
trigger: eventTrigger({
name: "user.created",
schema: z.object({
name: z.string(),
email: z.string(),
paidPlan: z.boolean(),
}),
}),
//this function is run when the custom event is received
run: async (payload, io, ctx) => {
//do stuff
},
});
So it will help to know a little about Zod and how to use it. We definitely recommend the well written Zod README but we’ve included a short primer below.
Wherever we require you to pass in a Zod schema, you can always start with z.any()
which accepts
any
type and then add more strict validations later.
Basic Usage
There are three main steps to using Zod:
- Define a schema
import { z } from "zod";
const mySchema = z.object({
name: z.string(),
email: z.string(),
paidPlan: z.boolean(),
});
- Infer TypeScript types from the schema
type MySchema = z.infer<typeof mySchema>;
- Validate data against the schema
const data: unknown = {
name: "Eric",
email: "[email protected]",
paidPlan: true,
};
const result = mySchema.parse(data);
When using Zod with Trigger.dev, you’ll only really need to do the first step, and by passing it to us (through the schema
property of the customEvent
function), we’ll do the second and third steps for you.
Defining Schemas
Primitives
Zod schemas are a way to define the shape of an object. They can be as simple as a single type, or as complex as a nested object.
// Primitives
z.string();
z.number();
z.boolean();
z.date();
z.undefined();
z.null();
Any schema can be marked as optional, which means the schema can be undefined
or null
:
z.string().optional();
Schemas can also be marked optional by providing a default value:
const optionalString = z.string().default("default value");
const value = optionalString.parse(undefined); // value === "default value"
If you need to allow a value to be null
, you can use nullable()
:
const nullableString = z.string().nullable();
const value = nullableString.parse(null);
You can also use Zod to coerce primites into other types. For example, you can coerce a string into a number:
const numberString = z.coerce.number();
const value = numberString.parse("123"); // value === 123
The following primitives are supported:
z.coerce.string();
z.coerce.number();
z.coerce.boolean();
z.coerce.bigint();
z.coerce.date();
Coercing dates are especially useful when you are receiving a string
from an API and want to convert it to a JavaScript Date
object:
const date = z.coerce.date();
const value = date.parse("2021-01-01T00:00:00.000Z"); // value === Date object
Objects
Object schemas are the most common type of schema. They allow you to define the shape of an object, and the types of each property.
const mySchema = z.object({
name: z.string(),
email: z.string(),
paidPlan: z.boolean(),
});
All properties are required by default, although you can make them all optional using partial()
:
const mySchema = z
.object({
name: z.string(),
email: z.string(),
paidPlan: z.boolean(),
})
.partial();
You can also make individual properties optional:
const mySchema = z.object({
name: z.string(),
email: z.string(),
paidPlan: z.boolean().optional(),
});
By default an object schema will strip out any extra properties that are not defined in the schema. You can disable this behavior using passthrough()
:
const mySchema = z.object({
name: z.string(),
});
mySchema.parse({ name: "Eric", email: "[email protected]" }); // { name: "Eric" }
mySchema.passthrough().parse({ name: "Eric", email: "[email protected]" }); // { name: "Eric", email: "[email protected]" }
Or you can use strict()
to make the schema throw an error if there are any extra properties:
const mySchema = z
.object({
name: z.string(),
})
.strict();
mySchema.parse({ name: "Eric", email: "[email protected]" }); // throws error
Zod includes a few useful object schema utilities to help with reusing schemas, extends()
and merge()
:
const baseSchema = z.object({
name: z.string(),
});
const extendedSchema = baseSchema.extend({
email: z.string(),
});
const mergedSchema = baseSchema.merge(
z.object({
email: z.string(),
})
);
You can also use pick()
and omit()
to create a new schema that only includes or excludes certain properties:
const mySchema = z.object({
name: z.string(),
email: z.string(),
paidPlan: z.boolean(),
});
const pickedSchema = mySchema.pick({ name: true, email: true });
const omittedSchema = mySchema.omit({ paidPlan: true });
Arrays
You can specify the schema of an array using z.array()
:
z.array(z.string()); // string[]
z.array(z.number()); // number[]
z.array(z.object({ name: z.string() })); // Array<{ name: string }>
You can also specify the type of array that has a fixed number of elements using z.tuple()
:
z.tuple([z.string(), z.number(), z.boolean()]); // [string, number, boolean]
Unions
You can specify a union of schemas using z.union()
:
z.union([z.string(), z.number(), z.boolean()]); // string | number | boolean
Discriminating unions are also supported, and especially useful when paired with type narrowing:
const mySchema = z.discriminatingUnion("type", [
z.object({
type: z.literal("a"),
data: z.string(),
}),
z.object({
type: z.literal("b"),
data: z.number(),
}),
]);
const value = mySchema.parse({ type: "a", data: "hello" });
if (type.a) {
// value is { type: "a", data: string }
} else if (type.b) {
// value is { type: "b", data: number }
}
Records
You can specify a record of schemas using z.record()
, useful for when you have a map of values but don’t care about the keys:
z.record(z.string()); // Record<string, string>
z.record(z.number()); // Record<string, number>
JSON type
If you want to accept any valid JSON value, you can use the following schema:
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
type Literal = z.infer<typeof literalSchema>;
type Json = Literal | { [key: string]: Json } | Json[];
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
);
jsonSchema.parse(data);
Hat tip to ggoodman for this one.
Resources
Matt Pocock @mattpocock has a great free Zod Tutorial up on Total Typescript that covers the basics of Zod.
Easily generate Zod schemas from JSON, JSON Schemas, or TypeScript types.
Was this page helpful?