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:

  1. Define a schema
import { z } from "zod";

const mySchema = z.object({
  name: z.string(),
  email: z.string(),
  paidPlan: z.boolean(),
});
  1. Infer TypeScript types from the schema
type MySchema = z.infer<typeof mySchema>;
  1. 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.