Skip to main content

Command Palette

Search for a command to run...

ZOD - TypeScript-first schema data validation

Updated
•5 min read•View as Markdown
ZOD - TypeScript-first schema data validation
A

I'm an aspiring frontend developer currently a 3rd college student at Indore. I am passionate about frontend web development, and UI/UX designing and recently I've taken a lot of interest in Opensource

Do you guys know that Typescript only does type-checking at compile time and no runtime checks?

Well, why do we even need runtime checks after typescript compile time checks, well the first use could be validating data on the server side, which typescript has no idea of at compile time.

we can't always be sure about what type of variables we'll get from external sources like APIs or form inputs.

For example, when we have some form to be filled, Typescript can't guarantee the user-filled inputs to be 100% type correct as we want on our database.

What is Zod and why do we need it?

Zod is a tool that solves this problem. It fills this TypeScript blindspot and helps with type safety during runtime by having an additional runtime check.

Zod is a TypeScript-first schema declaration and validation library. It provides safety checks at runtime working in conjunction with Typescript!

Installation

Zod can be installed into your react application using npm or yarn

npm install zod

yarn add zod

Requirements

  • TypeScript 4.5+!
  • strict mode must be enabled in your tsconfig.json file
    // tsconfig.json
    {
      "compilerOptions": {
        "strict": true
      }
    }
    

Usage

Let's string by creating a very simple string schema using zod

import { z } from "zod";

const stringSchema = z.string().min(10).max(15);

stringSchema.parse("example string")

This will be safely parsed as it follows the zod schema we used to parse it.

But if we try something that outflows the schema parsing instructions, such as

stringSchema.parse("string of much greater length")

This will throw an error

{
      code: 'too_big',
      maximum: 15,
      type: 'string',
      inclusive: true,
      message: 'String must contain at most 15 characters,
      path: []
    }

Zod also provides various other methods such as

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();

Objects in zod

Objects can be used extensively whether they be validating a user form or fetching data from an API

Let's take an example for validating a user form data

const userData = z.object({
  firstName: z.string().min(1).max(10),
  lastName: z.string().min(1).max(10),
  email: z.string().email(),
})

zod to parse API data

In this example let's fetch data from the random user API and validate it using zod

const randomUsers = async () => {
  const data = await fetch("https://swapi.dev/api/people/")
    .then((res) => res.json(),
  );
};

Now this would return an array of users, we can parse the result using the z.array() method

  const randomUsers = z.object({
  results: z.array(z.object()),
});

  const parsedData = randomUsers.parse(data);

  return parsedData.results;

Here we are declaring that the results array could be of any object type, we can further specify the data inside the result array to be of the user type like this

  const randomUser = z.object({
      name: z.object({
        first: z.string(),
        last: z.string()
      })
  })

// declare the type of results array to be randomUser

  const randomUsers = z.object({
  results: z.array(randomUser),
});

  const parsedData = randomUsers.parse(data);

  return parsedData.results;

Make schema values optional

What if we want our email input to be optional here, we can do it as such by adding .optional() to the end of our email schema:

const userData = z.object({
  firstName: z.string().min(1).max(10),
  lastName: z.string().min(1).max(10),
  email: z.string().email().optional(),
})

Default value in the schema

What if we want email to be optional but don't want it to be undefined in our database, we can do so by passing a default value in its place by adding .default() to the end of our email schema:

const userData = z.object({
  firstName: z.string().min(1).max(10),
  lastName: z.string().min(1).max(10),
  email: z.string().email().default("default@gmail.com"),
})

Reducing Duplicate values by composing schemas

Suppose we have this schema with User, Post, Comment

const User = z.object({
  id: z.string().uuid(),
  name: z.string(),
});

const Post = z.object({
  id: z.string().uuid(),
  title: z.string(),
});

const Comment = z.object({
  id: z.string().uuid(),
  comment: z.string(),
});

Notice that id is repeated in every schema

A very basic solution could be placing id in its own type and other schemas can use that reference.

const Id = z.string().uuid();

const User = z.object({
  id: Id,
  name: z.string(),
});

const Post = z.object({
  id: Id,
  title: z.string(),
  body: z.string(),
});

const Comment = z.object({
  id: Id,
  text: z.string(),
});

But this is still quite repetitive, Another solution can be by using the extend method.

We can create a base object schema with id and then extend all other schema and add to the base schema as such:

const schemaWithId = z.object({
  id: z.string().uuid()
});

const User = ObjectWithId.extend({
  name: z.string(),
});

const Post = ObjectWithId.extend({
  title: z.string(),
});

const Comment = ObjectWithId.extend({
  comment: z.string(),
});

now we can add multiple values directly to share between all the objects

const schemaWithId = z.object({
  id: z.string().uuid(),
  createdAt: z.datetime()
});

Both id and createdAt values will be shared between all the objects.

Transform Data within the schema

Let's take back the random user but with a slight change that we are receiving fullName instead of separate firstName and lastName but and we want to split the fullName value into firstName and lastName

This can be done by transforming the fullName value by splitting it at the " " blank space within the schema by using the transform method

const userData = z.object({
  fullName: z.string()
})
  .transform(user => ({
      ...user,
      nameAsArray: user.name.split( " " ),  
  }));

the transform method can be used in various ways to transform that data within the schema.

Type Inference

Suppose we already have defined our schema somewhere and want to use the same schema definition somewhere else without rewriting the schema again, we can do such by extracting its typescript type.

We can extract the TypeScript type of any schema with z.infer<typeof schema>


const user = z.object({
  name: z.string(),
  age: z.number(),
});

type userType = z.infer<typeof user>;

zod is a very easy library to implement and having both compile time and runtime checks will even boost your application's performance.

I hope these help you to get a basic understanding of how zod works. zod is much more than just this, to learn and understand much more about zod, use these official docs

Thanks for reading till the last

Reach me on Twitter