Client-side object validation with Yup

Client-side object validation with Yup

Intro to client-side data validation with Yup

·

5 min read

Introduction

Typescript introduced many positive things in JavaScript. When used right, it can help with having cleaner code and reducing bugs. However, it mainly works on the compile time, which means it helps you when writing code. Not when running it. And sometimes, you want to verify the structure of data on run time. If you have something simple, like checking if some value is a string, it is quite easy, but if you are validating some more complex structure with different data types. That can get much more complicated. For that, there are libraries like Yup, which I am going to cover in the rest of this post.

Image description

Installation

Yup installation is quite simple. All you need to do is either add it to your package.json file or run the following command:

npm run install -S yup

Basics

It all starts by defining schema, and yup comes with a whole range of built-in, flexible, options for basic types, and options to build more complex ones.

const schema = yup.object().shape({
  // object schema
});

In the code above, the result is yup schema. The parameter of the shape function is an empty object, so it does not test anything, but this is where you would pass validation details for your data. For that, let's consider the next values and build a validation object for them.

const data = {
  firstName: 'john',
  lastName: 'doe',
  age: 25,
  email: 'john.doe@email.com',
  created: new Date(2021, 5, 5)
}

Observing data, we can see that firstName, lastName, and email are strings, age is a number, and created is a date. Lucky for us, yup supports all those types, and shape objects could be defined like the following.

const schema = yup.object().shape({
  firstName: yup.string(),
  lastName: yup.string(),
  age: yup.number(),
  email: yup.string(),
  created: yup.date(),
});

Once we have both data and schema defined, data can be tested. For that, our schema has an isValid function that returns a promise that resolves with Boolean status.

const valid = schema.isValid(data).then((valid) => {
  console.log(valid); // true
});

Additional validation

The example above is ok. But what if we need additional validations? What if lastName is missing? And email is defined as a string but not any string is email. Also, how about age, maybe there is a minimum and maximum value? Again, yup has the option to support all those requirements. For example, let us consider the following example of invalid data.

const data = {
  lastName: "a",
  age: 80,
  email: "blob",
  created: "Some invalid string"
}

In the above, this would be a valid object. However, we could test all the additional requirements by using the next schema.

const schema = yup.object().shape({
  firstName: yup.string().required(),
  lastName: yup.string()
                .required("Last name is required").min(2),
  age: yup.number()
          .required()
          .min(30)
          .max(50, "Value needs to be less than 50"),
  email: yup.string().required().email(),
  created: yup.date().required(),
});

The first thing above you might notice is that all have .required() call. This defines that a field is required, and it accepts an optional parameter of string which would be used instead of the default message. Other interesting calls are min and max. These depend on the data type. For numbers, it is defining minimum or maximum number value, and for strings, it defines length requirements. Almost all additional functions do take custom error messages as optional extra parameters.

Errors

In the above example, we determined if the schema is valid. But what if it isn't? You would want to know some details about it. Error messages and fields that are invalid and this is in my opinion biggest issue of yup. There is a method called validate, but getting fields and errors is not so straightforward from the documentation. Validate is an async function that returns a promise and resolves if valid, or rejects if invalid data. This can be caught by using two ways below (await is also an option and there are synchronized versions of these functions as well).

schema.validate(data).then(
  () => { /* valid data function */ },
  () => { /* invalid data function */ }
);

schema.validate(data).catch(errors => {
  // validation failed
});

The problem mentioned above is that yup exits when it finds the first error, so it won't register all fields that do have it. But for that, we could pass the options object with the abortEarly field set to false. And this brings us to the next problem, which is getting data out of the error. In the error function, the parameter we get is an errors object which has inner property, and that property has a value of an array containing all errors, and as part of each error, we get a path property containing the field name and errors containing all error messages for that specific error.

schema.validate(invalidData, { abortEarly: false }).catch(function(errors) {
  errors.inner.forEach(error => {
    console.log(error.path, error.errors)
  })
});

Nested data types

These above were all simple fields. But yup also contains complex ones like nested objects and arrays. I won't go into them too much, but you can see simple examples below.

let numberSchema = yup.array().of(yup.number().min(2));
let nestedObjectSchema = yup.object().shape({
  name: yup.string(),
  address: yup.object().shape({
    addressLine1: yup.string(),
    addressLine2: yup.string(),
    town: yup.string(),
    country: yup.string(),
  })
})

Wrap up

Yup is a great library with many uses. And there are many more options than shown in this post. But I hope from this post you got some basic intro into understanding it, and all examples you can find also in my GitHub repository.


For more, you can follow me on Twitter, LinkedIn, GitHub, or Instagram.