DEV Community

Cover image for Learn "Zod" In 5 Minutes
Arafat
Arafat

Posted on • Updated on

Learn "Zod" In 5 Minutes

Goals of Zod

  1. Validation library (Schema first)
  2. First class typescript support (No need to write types twice)
  3. Immutable (Functional programming)
  4. Super small library (8kb)

Setup

Can be used with Node/Deno/Bun/Any Browser etc.

npm i zod
import { z } from "zod";
Must have strict: true in tsconfig file


Basic Usage

// creating a schema
const UserSchema = z.object({
  username: z.string(),
});

// extract the inferred type
type User = z.infer<typeof UserSchema>;
// { username: string }

const user: User = {username: "Arafat"}

// parsing
UserSchema.parse(user); // => {username: "Arafat"}
UserSchema.parse(12); // => throws ZodError

// "safe" parsing (doesn't throw error if validation fails)
UserSchema.safeParse(user); 
// => { success: true; data: {username: "Arafat"} }

UserSchema.safeParse(12); 
// => { success: false; error: ZodError }
Enter fullscreen mode Exit fullscreen mode

Basic Types

import { z } from "zod";

// primitive values
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
z.symbol();

// empty types
z.undefined();
z.null();
z.void(); // accepts undefined

// catch-all types
// allows any value
z.any();
z.unknown();

// never type
// allows no values
z.never();
Enter fullscreen mode Exit fullscreen mode

Validations

All types in Zod have an optional options parameter you can pass as the last param which defines things like error messages.

Also many types has validations you can chain onto the end of the type like optional

z.string().optional()
z.number().lt(5)
optional() - Makes field optional
nullable - Makes field also able to be null
nullish - Makes field able to be null or undefined

Some of the handful string-specific validations

z.string().max(5);
z.string().min(5);
z.string().length(5);
z.string().email();
z.string().url();
z.string().uuid();
z.string().cuid();
z.string().regex(regex);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().trim(); // trim whitespace
z.string().datetime(); // defaults to UTC, see below for options
Enter fullscreen mode Exit fullscreen mode

Some of the handful number-specific validations

z.number().gt(5);
z.number().gte(5); // alias .min(5)
z.number().lt(5);
z.number().lte(5); // alias .max(5)

z.number().int(); // value must be an integer

z.number().positive(); //     > 0
z.number().nonnegative(); //  >= 0
z.number().negative(); //     < 0
z.number().nonpositive(); //  <= 0

z.number().multipleOf(5); // Evenly divisible by 5. Alias .step(5)

z.number().finite(); // value must be finite, not Infinity or -Infinity
Enter fullscreen mode Exit fullscreen mode

Default Values

Can take a value or function.
Only returns a default when input is undefined.

z.string().default("Arafat")
z.string().default(Math.random)


Literals

const one = z.literal("one");

// retrieve literal value
one.value; // "one"

// Currently there is no support for Date literals in Zod.
Enter fullscreen mode Exit fullscreen mode

Enums

Zod Enums

const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);

type FishEnum = z.infer<typeof FishEnum>;
// 'Salmon' | 'Tuna' | 'Trout'

// Doesn't work without `as const` since it has to be read only
const VALUES = ["Salmon", "Tuna", "Trout"] as const;
const fishEnum = z.enum(VALUES);

fishEnum.enum.Salmon; // => autocompletes

TS Enums: (You should use Zod enums when possible)

enum Fruits {
  Apple,
  Banana,
}
const FruitEnum = z.nativeEnum(Fruits);

Objects

z.object({})

// all properties are required by default
const Dog = z.object({
  name: z.string(),
  age: z.number(),
});

// extract the inferred type like this
type Dog = z.infer<typeof Dog>;

// equivalent to:
type Dog = {
  name: string;
  age: number;
};
Enter fullscreen mode Exit fullscreen mode

.shape.key - Gets schema of that key

Dog.shape.name; // => string schema
Dog.shape.age; // => number schema
Enter fullscreen mode Exit fullscreen mode

.extend - Add new fields to schema

const DogWithBreed = Dog.extend({
  breed: z.string(),
});
Enter fullscreen mode Exit fullscreen mode

.merge - Combine two object schemas

const BaseTeacher = z.object({ students: z.array(z.string()) });
const HasID = z.object({ id: z.string() });

const Teacher = BaseTeacher.merge(HasID);
type Teacher = z.infer<typeof Teacher>; // => { students: string[], id: string }
Enter fullscreen mode Exit fullscreen mode

.pick/.omit/.partial - Same as TS

const Recipe = z.object({
  id: z.string(),
  name: z.string(),
  ingredients: z.array(z.string()),
});


// To only keep certain keys, use .pick
const JustTheName = Recipe.pick({ name: true });
type JustTheName = z.infer<typeof JustTheName>;
// => { name: string }


// To remove certain keys, use .omit
const NoIDRecipe = Recipe.omit({ id: true });
type NoIDRecipe = z.infer<typeof NoIDRecipe>;
// => { name: string, ingredients: string[] }


// To make every key optional, use .partial
type partialRecipe = Recipe.partial();
// { id?: string | undefined; name?: string | undefined; ingredients?: string[] | undefined }
Enter fullscreen mode Exit fullscreen mode

.deepPartial - Same as partial but for nested objects

const user = z.object({
  username: z.string(),
  location: z.object({
    latitude: z.number(),
    longitude: z.number(),
  }),
  strings: z.array(z.object({ value: z.string() })),
});

const deepPartialUser = user.deepPartial();

/*
{
  username?: string | undefined,
  location?: {
    latitude?: number | undefined;
    longitude?: number | undefined;
  } | undefined,
  strings?: { value?: string}[]
}
*/
Enter fullscreen mode Exit fullscreen mode

passThrough - Let through non-defined fields

const person = z.object({
  name: z.string(),
});

person.parse({
  name: "bob dylan",
  extraKey: 61,
});
// => { name: "bob dylan" }
// extraKey has been stripped


// Instead, if you want to pass through unknown keys, use .passthrough()
person.passthrough().parse({
  name: "bob dylan",
  extraKey: 61,
});
// => { name: "bob dylan", extraKey: 61 }
Enter fullscreen mode Exit fullscreen mode

.strict - Fail for non-defined fields

const person = z
  .object({
    name: z.string(),
  })
  .strict();

person.parse({
  name: "bob dylan",
  extraKey: 61,
});
// => throws ZodError
Enter fullscreen mode Exit fullscreen mode

Arrays

const stringArray = z.array(z.string()); - Array of strings

.element - Get schema of array element

stringArray.element; // => string schema
Enter fullscreen mode Exit fullscreen mode

.nonempty - Ensure array has a value

const nonEmptyStrings = z.string().array().nonempty();
// the inferred type is now
// [string, ...string[]]

nonEmptyStrings.parse([]); // throws: "Array cannot be empty"
nonEmptyStrings.parse(["Ariana Grande"]); // passes
Enter fullscreen mode Exit fullscreen mode

.min/.max/.length - Gurantee certail size

z.string().array().min(5); // must contain 5 or more items
z.string().array().max(5); // must contain 5 or fewer items
z.string().array().length(5); // must contain 5 items exactly
Enter fullscreen mode Exit fullscreen mode

Advanced Types

Tuple

Fixed length array with specific values for each index in the array

Think for example an array of coordinates.

z.tuple([z.number(), z.number(), z.number().optional()])

.rest - Allow infinite number of additional elements of specific type

const variadicTuple = z.tuple([z.string()]).rest(z.number());
const result = variadicTuple.parse(["hello", 1, 2, 3]);
// => [string, ...number[]];

Union

Can be combined with things like arrays to make very powerful type checking.

let stringOrNumber = z.union([z.string(), z.number()]);
// same as
let stringOrNumber = z.string().or(z.number());

stringOrNumber.parse("foo"); // passes
stringOrNumber.parse(14); // passes

Discriminated unions

Used when one key is shared between many types.

Useful with things like statuses.

Helps Zod be more performant in its checks and provides better error messages

const myUnion = z.discriminatedUnion("status", [
  z.object({ status: z.literal("success"), data: z.string() }),
  z.object({ status: z.literal("failed"), error: z.instanceof(Error) }),
]);

myUnion.parse({ status: "success", data: "yippie ki yay" });


Records

Useful when you don't know the exact keys and only care about the values

z.record(z.number()) - Will gurantee that all the values are numbers

z.record(z.string(), z.object({ name: z.string() })) - Validates the keys match the pattern and values match the pattern. Good for things like stores, maps and caches.


Maps

Usually want to use this instead of key version of record

const stringNumberMap = z.map(z.string(), z.number());

type StringNumberMap = z.infer<typeof stringNumberMap>;
// type StringNumberMap = Map<string, number>
Enter fullscreen mode Exit fullscreen mode

Sets

Works just like arrays (Only unique values are accepted in a set)

const numberSet = z.set(z.number());
type NumberSet = z.infer<typeof numberSet>;
// type NumberSet = Set<number>
Enter fullscreen mode Exit fullscreen mode

Promises

Does validation in two steps:

  1. Ensures object is promise
  2. Hooks up .then listener to the promise to validate return type.
const numberPromise = z.promise(z.number());

numberPromise.parse("tuna");
// ZodError: Non-Promise type: string

numberPromise.parse(Promise.resolve("tuna"));
// => Promise<number>

const test = async () => {
  await numberPromise.parse(Promise.resolve("tuna"));
  // ZodError: Non-number type: string

  await numberPromise.parse(Promise.resolve(3.14));
  // => 3.14
};
Enter fullscreen mode Exit fullscreen mode

Advanced Validation

.refine

const email = z.string().refine((val) => val.endsWith("@gmail.com"),
{message: "Email must end with @gmail.com"}
)
Enter fullscreen mode Exit fullscreen mode

Also you can use the superRefine method to get low level on custom validation, but most likely won't need it.

Handling Errors

Errors are extremely detailed in Zod and not really human readable out of the box. To get around this you can either have custorm error messages for all your validations, or you can use a library like zod-validation-error which adds a simple fromZodError method to make error human readable.

import { fromZodError } from "zod-validation-error"

console.log(fromZodError(results.error))
Enter fullscreen mode Exit fullscreen mode

Conclusion

There are many more concepts of Zod, and I can't explain all that stuff here. However, If you want to discover them, head to Zod's official documentation. They've explained everything perfectly there.

So, this was it, guys. I hope you guys will like this crash course. I've tried my best to pick all of the essential concepts of Zod and explain them. If you have any doubts or questions, then feel free to ask them in the comment section. I will answer as soon as I see it. See you all in my next article😊.

Visit:
đŸ‘šâ€đŸ’»My Portfolio
đŸžïžMy Fiverr
🌉My Github
đŸ§™â€â™‚ïžMy LinkedIn

Top comments (5)

Collapse
 
magnuspladsen profile image
Magnus Pladsen

Very nice! I'm currently using Yup on a project at work, but kinda sad i did now know about Zod before. This looks much simpler and easier to work with! Thanks for the guide!

Collapse
 
thestope profile image
Sam

What is mySchema? What does parse do, and why does it turn an object you've just declared as {username:Arafat} into the single string "tuna"?

I'm being facetious but some of these examples could use a bit more context.

Collapse
 
arafat4693 profile image
Arafat • Edited

Sorry, I think that was a mistake by me. It is now fixed😊

Collapse
 
josuemb profile image
JosuĂ© MartĂ­nez Buenrrostro • Edited

Nice article. Very useful to introduce anyone to zod.
There is a typo in point 3 of the section "Goals of zod" it is written as "porgramming" instead of "programming".

Collapse
 
arafat4693 profile image
Arafat

Thanks for letting me know😊