Comparing schema validation libraries: Zod vs. Yup

Posted on June 05, 2020

feature-image.png

Web developers have to deal with a lot of complex data exchanges in their applications. It’s important for apps to validate any data they send or receive because any wrong data type can cause an error. Unexpected data types or values can also crash the application processing it and lead to other undesirable outcomes that you want to avoid at all costs.

Schema validation is the process of verifying data by creating a schema. The definition of a schema can vary depending on the implementation, but in JavaScript, a schema is usually an object data type that is used to validate the data in question.

Below is a simple example.

var userDataSchema = {
  name: "string",
  age: "number"
}

var userData = {
  name: "Peter",
  age: "15"
}

function validateSchema(schema, data){
  for (var property in schema) {
    if(data[property] === undefined){
      console.error(property + " is required.")
    } else if ( typeof data[property] !== schema[property] ){
      console.error(property+ " must be of type "+ schema[property] + ", " +typeof data[property]+ " found.")
    } else {
      console.log(property + " is validated!")
    }
  }
}

validateSchema(userDataSchema, userData)

In the code above, the userDataSchema object acts as a schema, validating that the userData object has properties of name and age. It also validates the type of these values: name must be a string while age must be a number.

Of course, the validateSchema function is not enough for any serious application. To validate a complex data structure and ensure that it doesn’t cause unwanted behavior, you’ll need something more powerful. This is where a schema validation library comes in.

Fortunately, generous JavaScript developers around the world have shared myriad open-source schema validation libraries on npm. By far, Yup has been one of the most popular schema validation libraries for JavaScript. But a new validation library recently came onto the scene and has been stealing some of Yup’s spotlight since it was released in March.

Zod is a schema validation library designed to enhance the developer experience when dealing with schema validations. In this guide, I’ll compare these libraries to see how they perform in terms of validating common data patterns such as primitives, objects, and arrays.

First, let’s review some general library stats from npm trends that I took on April 19, 2020.

[feature-image.png zod-yup-npm-trends-1.png]
Zod vs Yup NPM trends
Zod vs Yup NPM trends

Yup, which was released six years ago, is obviously a more popular choice at this time. Zod is smaller in size, probably because it doesn’t have as many APIs as Yup, but it’s sure to grow as more features are developed.

Basic object validations

Let’s start by comparing how to validate a simple object and its properties with each service.

Below is how to start validating using Yup.

let yup = require('yup')

let schema = yup.object({
  name: yup.string().required(),
  age: yup.number()
})

schema
.isValid({
  name: 'John',
  age: true // wrong value
})
.then(function(valid) {
  console.log(valid)
})

We created an object schema with Yup’s .object function. This schema has two properties:

  • A name property that is a string type and is required
  • An age property that is of number type but is not required

After the schema is ready, we validated the data by calling the .isValid function. We put an object with the age property as a boolean type instead of a number, which caused the function to return false.

Here’s how to do validations with Zod.

const userSchema = zod.object({
  name: zod.string(),
  age: zod.number().optional(),
})

userSchema.parse({
  name: "Jane",
  age: true // wrong value
})

Unlike Yup, Zod assumes all validation is required by default. If you want to make it optional, chain your validation with the .optional() function.

Both validations appear identical at first, but Zod is actually parsing the object instead of validating it. This means that Zod takes the given data and tries to return it back. If the parser encounters an error, it will terminate the runtime and throw an error.

Whereas Yup only validates the data and leaves error handling to you, Zod takes validation one step further by throwing an error. Making sure the program you wrote can’t continue running. If you want to catch the error, enclose the parsing function inside a try-catch block.

try {
  userSchema.parse({
    name: "Jane",
    age: true // wrong value
  })
} catch (err) {
  console.log(err.message)
}

Now you can have the error gracefully logged into the console.

Primitive data types

Now let’s compare how the libraries validate primitive data types. Here’s how to validate a string with Yup:

let schema = yup.string()

schema.isValid('hello')
.then(function(valid) {
  console.log(valid)
})

Let’s do the same with Zod.

let schema = zod.string()

try {
  schema.parse('hello')
} catch (err) {
  console.log(err.message)
}

If the parsing doesn’t get through, an error is logged. So far, both Zod and Yup seem capable of validating primitive data types. What’s more, both can also check if your string is a valid email address:

let yup = require('yup')

let schema = yup.string().email() // validate if the string is also a valid email address format

schema.isValid('hello@mail.com')
.then(function(valid) {
  console.log(valid)
})

You need to validate email addresses with Zod manually. You can use regex, as shown below.

let zod= require('zod')

let schema = zod.string().email()

try {
  schema.parse('hellomail.com')
} catch (err) {
  console.log(err.message)
}

Zod has implemented a wide arrange of extensions for validating data types. In the example below, it validates that the data is of number type and has a positive value — an integer instead of a float.

let schema = zod.object({
  age: zod
    .number()
    .positive()
    .int()
})

Yup also has its own number schema with a bit more validation like truncate and round, but since Zod is under very active development, it might catch up pretty soon.

Literal validation

Below is how to perform literal validation with Yup.

let schema = yup.mixed().oneOf(['Tomato'])

schema.isValid('Tomato')
.then(function(valid){
  console.log(valid)
})

Yup’s mixed function can match all types, and by using the oneOf function, you can input a value that is taken literally.

Here’s what literal validation looks like with Zod:

let schema = zod.literal('Tomato')

try {
  schema.parse('Tomato')
} catch (err) {
  console.log(err.message)
}

Literal validation in Zod is simpler because it has a literal function that you can use to do it.

Array validation

For array type, Yup has several useful extensions to validate its values. For example, you can validate the minimum or maximum length of the array with the .min and .max functions. You can also check the type of its value with the .of function.

// validate that the data is an array with number as its value.
// The minimum value of the array is two
// The minimum length of the array is four
let schema = yup.array().of(yup.number().min(2)).min(4);

schema.isValid([2])
.then(function(valid) {
  console.log(valid) // false
})

Zod also can validate an array just like Yup does, but with a small difference in its syntax:

let schema = zod.array(zod.string()).min(2).max(5)

try {
  schema.parse(['fish', 'meat', 'banana'])
} catch (err) {
  console.log(err.message) // Error non-number type
}

Function validation

Zod can validate a function and make sure its input and output type is correct. The function schema accepts two parameters: the arguments (args) in the form of a tuple and the function’s return type.

A tuple is another special Zod API that creates an array with a fixed number of elements and various data types.

const athleteSchema = zod.tuple([
  // takes an array of schemas
  zod.string(), // a string for name
  zod.number(), // a number for jersey
  zod.object({
    pointsScored: zod.number(),
  }), // an object with property pointsScored that has number value
]);
try {
  athleteSchema.parse(["James", 23, { pointsScored: 7 }])
} catch (err) {
  console.log(err.message)
}

The data parsed into the tuple must be an array that exactly matches the schema structure. By using a tuple, you can pass as many arguments into your function as you need.

Below is an example of code for the function schema. It takes two numbers as arguments and returns a string.

const args = zod.tuple([
  zod.number(), // arg1
  zod.number() // arg2
])
const returnType = zod.string()
const fnSumSchema = zod.function(args, returnType)

const mySum = fnSumSchema.validate((arg1, arg2) => {
  return arg1 + arg2 // TypeError. Should be string
})
const sumAsString = mySum(3, 4)

Unlike other validations we’ve seen so far, function validation in Zod doesn’t use the same .parse to validate the function.

Function validation is unique to Zod; Yup doesn’t have an equivalent API to perform this task.

TypeScript support

Both libraries support TypeScript. Zod offers TypeScript first-class support. These libraries enable you to infer TypeScript type aliases that you can use to validate the data.

In simple terms, you can validate whether a variable is the correct type of data by creating a type alias from Yup or Zod’s schema.

import * as yup from "yup";
import * as zod from "zod";

const yupSchema = yup.string()
type A = yup.InferType<typeof yupSchema>
const x: A = 12 // wrong, but nothing happens

const zodSchema = zod.string();
type B = zod.infer<typeof zodSchema>; // string
const y: B = 12; // TypeError

You can run the script above using TypeScript. Notice that Zod actually throws an error while Yup does nothing, even though the value of x should be a string instead of a number.

Zod’s union function

Zod also has some unique APIs to define optional schema. For example, the union method can be used to compose “OR” types. For example, to create a schema where the data is a string “OR” a number:

let zod= require('zod')

const stringOrNumber = zod.union([zod.string(), zod.number()]);

try {
  stringOrNumber.parse({});
} catch (err) {
  console.log(err.message) // Error non-string, non-number type
}

Zod’s intersection function

Another one of Zod’s unique API is the intersection method, which is particularly useful for combining two schemas, creating a “schema mixin.” For example:

let zod= require('zod')

const HasId = zod.object({
  id: zod.number(),
});

const BaseTeacher = zod.object({
  name: zod.string(),
});

const Teacher = zod.intersection(BaseTeacher, HasId);

type Teacher = zod.infer<typeof Teacher>;
// { id:number; name:string };

Conclusion

As you can see from the comparisons above, Zod and Yup both have simple APIs to validate data using schema. Yup has some functions outside of validating data, such as the number schema’s truncate and round methods, which might comes in handy in a specific situation.

Just like Yup, Zod is capable of validating a function’s input and output to make sure it has all the right data. It also has great TypeScript support, which terminates the runtime in case of errors, while Yup simply does nothing when the inferred type is wrong. What’s more, Zod has some unique features to define optional schemas like union and intersection.

So which schema validation library should you use for your next project? It depends heavily on your application requirements. I recommend using Yup if you do a lot of form validation because its extensive functions cover many patterns that are used in forms, even situational ones where you have to do a rounding.

But if you have lots of API data exchange and you need to validate all data that passes between client and server, Zod might be your best bet — especially if you’re using TypeScript.

You can share this post with a friend:

WhatsAppLinkedInReddit