Jun 4, 2020

Validating Optional Objects with Yup

When it comes to choosing a client-side object validation library, Yup has increasingly become the go-to choice. With its descriptive, yet powerful API, Yup can be bent to most use cases. That said, those familiar with the NodeJS library Joi may be quick to notice the omission of a common API function: optional().

On a recent project, we were using Formik with Yup to build out highly dynamic, schema-generated forms. For simple forms, this worked great, but as the forms became more complex and the data models behind them grew to include a variety of different data types, the lack of a clear approach to optional object validation in Yup became an issue. While looking into this, I came across several solutions that might work for you, depending on your requirements.

For the TLDR version, feel free to jump to my CodeSandbox, which contains most of the following code examples.

The Problem

The simple version of the problem we were grappling with was this – how do we define an object as optional, but allow the object to have required properties within it?

  const yupSchema = {
    yup.object({
      // We want color to be optional, but IF there is data
      // for any of its properties, it should be treated 
      // as required
      color: yup.object({
        default: yup.string().required(),
        dark: yup.string().required(),
        light: yup.string().required()
      })
    })
  }

An example of valid and invalid values:

  // Valid values
  const noColor = {};
  const emptyColor = { color: {} };
  const optionalColor = {
    color: {
      dark: "",
      default: undefined,
      light: null
    }
  };
  const validColor = {
    color: {
      dark: "#951C22",
      default: "#404040",
      light: "#DD2B2E"
    }
  };

  // Invalid 
  const invalidColor = { 
    color: {
      // Color is missing required items
      light: '#DD2B2E'
    }
  };

Which can be summed up as:

  if (
    // We have a truthy value
    value &&
    // and any property of `color` has a value we care about
    Object.values(value).some(v => !(v === null || v === undefined || v === ""))
  ) {
    // Validate children properties
  } else {
    // Valid
  }

While researching options, I came across a variety of approaches that worked to varying degrees (sometimes our specific use case was a limiting factor).

Solution One? – notRequired

Being familiar with Joi validation library, my first instinct was to search the Yup documentation for an optional API, but I came up empty. I did, however, notice that there is a notRequired API, so I hoped that this would accomplish what I wanted.

  const yupSchema = {
    yup.object({
      color: yup.object({
        default: yup.string().required(),
        dark: yup.string().required(),
        light: yup.string().required()
      })
      // Appending this should do it right?
      .notRequired()
    })
  }

Unfortunately, this didn’t work as I’d hoped.

First, notRequired simply makes is so that undefined values do not fail validation. In the case of object types, Yup has a default of {}, so on its own notRequired doesn’t do much for us in this scenario. Yup does allow us to define a default value instead of using {}, but more on that in the next section.

The second issue was more specific to this use case – we were already defining our own defaults and passing them in via Formik’s forms. This meant by the time our form was rendered, and we wanted to start validating, we’d already populated the form with data we knew Yup wasn’t going to like.

  // Default data from the form after the initial render
  const formData = {
    color: {
      dark: "",
      default: "",
      light: ""
    }
  }

Solution One – Override the Default Value (aka the “recommended” approach)

With the base documentation not leading me to an obvious solution, it was time to dig deeper into GitHub issues and StackOverflow. Two GitHub issues that immediately jumped out were #532 “Nested object optional validations” and #678 “Nested object(): validation ignores required()”. Both suggested similar solutions – in combination with notRequired, override the type default to be undefined. (Note, the guidance on this approach comes from project author Jason Quense himself.)

  const yupSchema = {
    yup.object({
      color: yup.object({
        default: yup.string().required(),
        dark: yup.string().required(),
        light: yup.string().required()
      })
      .notRequired()
      // If color is undefined, leave it as undefined instead of casting it to {} 
      .default(undefined)
    })
  }

For a simple use case, this works, but it doesn’t quite satisfy all of my desired tests. This time, the underlying issue is our own. As mentioned above, we’re getting our values from a combination of our own defaults and those returned from Formik, so color never makes it to Yup as undefined.

For many projects, this sort of approach should work, but in our case, it was time to get more hands-on.

Solution Two – Lazy Validation

Since it is our data making things difficult, we needed to get more involved in the validation process. While looking into how we might go about enforcing our own rules, I came across a blog post by Jason Brown at CodeDaily.io descriptively named “How to Create an Optional Dynamic Validation Schema based on a Value with the Yup Validation Library”. In his post, Jason hits on the idea of using Yup’s .lazy() API to selectively run validation based on the specific value we have at the time of validation. While his final validation approach ends up being quite similar in function to using .notRequired().default(undefined) , it did get me moving in a direction that met all of my test criteria.

  const yupSchema = {
    yup.object({
      // Selectively apply validation at test time based off of the value
      color: yup.lazy(value => {
        // This is the same if-logic I outlined in "The Problem" section of this post
        if (
          value &&
          Object.values(value).some(v => !(v === null || v === undefined || v === ""))
        ) {
          // Return our normal validation
          return yup.object({
            default: yup.string().required(),
            dark: yup.string().required(),
            light: yup.string().required()
          })
        }
        // Otherwise, return a simple validation
        return yup.mixed().notRequired();
        // Note that the below code is also a valid final return
        // return yup.notRequired().default(undefined);
      })
    })
  }

Ultimately, this approach satisfies all of the “valid” scenarios I laid out at the start of this post! With a working solution on hand, there was one more approach I wanted to try that might better fit with how we were dynamically creating our forms and Yup validation.

Solution Three – Transform the Data Before Testing

Early in my search, I had dabbled with trying to use Yup’s .test() API to help in solving this problem. While that approach didn’t end up working for me, I stumbled across GitHub issue #242 “Question: .transform’s usage?” that clarified the phases Yup takes when validating – transform then test. Instead of swapping out the validation tests by using lazy, we can handle the noise that our custom defaults and form data is creating by applying our own transform function. This allows us to keep our actual validations much simpler to read and generate.

  const yupSchema = {
    yup.object({
      color: yup.object({
        default: yup.string().required(),
        dark: yup.string().required(),
        light: yup.string().required()
      })
      // Transform the value prior to testing
      .transform(value => {
        // If any child property has a value, skip the transform
        if (
          value &&
          Object.values(value).some(v => !(v === null || v === undefined || v === ""))
        ) {
          return value;
        }

        // Transform the value to undefined
        return undefined;
      })
    })
    .default(undefined)
  }

Turns out, transform isn’t much better than the lazy approach when it comes to trying to reuse our logic. It is roughly the same amount of code, but we’ve been able to isolate the core logic.

End Result – Custom .optional()

With the logic now isolated from the base schema, we can take advantage of Yup’s addMethod and create our own .optional() function.

  // Only add our `optional` method to object types
  yup.addMethod(yup.object, "optional", function(
    isOptional = true,
    defaultValue = undefined
  ) {
    return this.transform(function(value) {
      // If false is passed, skip the transform
      if (!isOptional) return value;

      // If any child property has a value, skip the transform
      if (
        value &&
        Object.values(value).some(v => !(v === null || v === undefined || v === ""))
      ) {
        return value;
      }

      return defaultValue;
    })
    // Remember, since we're dealing with the `object` 
    // type, we have to change the default value
    .default(defaultValue);
  });

Now that we have a custom method added to Yup, our color schema becomes much more straightforward, both to read and for us to generate!

  const yupSchema = {
    yup.object({
      color: yup.object({
        default: yup.string().required(),
        dark: yup.string().required(),
        light: yup.string().required()
      })
    })
    // To use our new method, this is all we have to do!
    .optional()
  }

Wrap Up

I hope some of the options I covered here will turn out to be useful. When I started looking into this problem, I assumed a quick solution was to be found. Since this didn’t turn out to be the case, I wanted to pull together the variety of options that can be used to accomplish supporting optional objects with Yup. Once again, code for most of the above examples, along with some unit tests, are running on this CodeSandbox – see which solution works for you!

About the Author

Daniel Testa profile.

Daniel Testa

Principal Technologist

A frontend developer with a focus on Javascript technologies.

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Blog Posts
Using Conftest to Validate Configuration Files
Conftest is a utility within the Open Policy Agent ecosystem that helps simplify writing validation tests against configuration files. In a previous blog post, I wrote about using the Open Policy Agent utility directly to […]
SwiftGen with Image & Color Asset Catalogs
You might remember back in 2015 when iOS 9 was introduced, and we were finally given a way to manage all of our assets in one place with Asset Catalogs. A few years later, support […]
Tracking Original URL Through Authentication
If you read my other post about refreshing AWS tokens, then you probably have a use case for keeping track of the original requested resource while the user goes through authentication so you can route […]
Using Spring Beans in a Kafka Streams ExceptionHandler
There are many things to know before diving into Kafka Streams. If you haven’t already, check out these 5 things as a starting point. Bullet 2 mentions designing for exceptions. Ironically, this seems to be […]