A Functional Approach to Data Validation

Validating data usually starts out simple, but as requirements change and your app grows, validation logic can quickly become complex and messy. This article proposes a solution that is readable, maintainable, and reusable.

Get the demo code.


Here's an example of validation in it's early stages. The rules are simple, and only several properties need to be checked. The assumption here is that all the relevant data will be passed into the validator as properties of an object.

function validator(obj) {  
  if(obj.car !== "Fiat") {
    alert("This club is for Fiat lovers only.");
    return false; 
  } else if(obj.chocolateType !== "milk") {
    alert("If you prefer dark chocolate, we prefer you not belong to our club.");
    return false;
  } else {
    return true;
  }
}

As is, the code above is fine. If more conditions were added, however, a refactor would be in order.

A good way to make validation logic maintainable is to compose validation functions. This means breaking up complex validation functions into simple rules. These rules can then be combined ('composed') in a validation function. If validation logic needs to change, you only need to refactor individual rules.

In our example, the validation checks whether a potential new club member is a good fit for the club. Here's an example of a function that acts a as a rule that can be combined in a validation function:

function favoriteCarIsFiat({ obj, errorArray = [] }) {  
  if (obj.car && obj.car.toLowerCase() === "fiat") {
    return {
      obj,
      errorArray
    };
  } else {
    return {
      obj,
      errorArray: [
        ...errorArray,
        "This club is for Fiat lovers only."
      ]
    };
  }
}

The function takes two arguments: an object with all the properties that need to be validated, and an array. The array will hold all the validation errors that occur. This way, if there are problems, the UI can iterate over the array and create a notification for each problem. Validation passes if errorArray.length === 0 or, put differently, !errorArray.length.

But I've jumped ahead. Above we see only one rule – how can we compose a validation function from many rules? If you're already using Lodash in your project, you can use its pipe() method. pipe() allows you to compose a function from other functions.


If you're not using Lodash, you can create your own pipe function with the code below:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);  

To demonstrate how pipe works, we need some more rules (i.e. functions that check only one condition) to work with. Suppose we have two more validation rules like the ones below:

function milkOrDarkChocolate({ obj, errorArray = [] }) {  
  if (obj.chocolateType && obj.chocolateType.toLowerCase() === "milk") {
    return {
      obj,
      errorArray
    };
  } else {
    return {
      obj,
      errorArray: [
        ...errorArray, 
        "This maniac prefers dark chocolate."
      ]
    };
  }
}

function favoriteAnimal({ obj, errorArray = [] }) {  
  if (obj.favoriteAnimal && obj.favoriteAnimal.toLowerCase() === "cats") {
    return {
      obj,
      errorArray
    };
  } else {
    return {
      obj,
      errorArray: [
        ...errorArray,
        "Not a cat lover. Don't let them in the club."
      ]
    };
  }
}

These rules check the preferred type of chocolate, and favorite animal. We can create a validation function that pipes arguments through all three rule functions. The pipe function takes the values returned from one function, and passes those values as arguments for the next function. In other words, it creates a pipeline of functions. Here's the code for creating our validation function.

const newMemberValidator = pipe(  
  favoriteCarIsFiat,
  milkOrDarkChocolate,
  favoriteAnimal
);

Each rule checks to see if the specified property exists (obj.car, obj.milkOrDarkChocolate, and obj.favoriteAnimal), and then checks the value assigned to the property. As the data is passed through the validation pipeline, failure messages are collected in the errorArray constant.

At this point, I encourage you to clone the repo and play with the code. The following object is sent through the validator in the repo.

const newMemberData = {  
  car: "Fiat",
  chocolateType: "milk",
  favoriteAnimal: "cats"
};

To validate the newMemberData, pass it into the validation function.

newMemberValidator(newMemberData);  

As is, this object passes validation. Go ahead and make it fail. Add more rules. In general, mess around with this strategy and see if it could useful in your projects.

Final Thoughts

In addition to making your code more readable and maintainable, composing your validation functions can also help you organize your code base. You can have a folder with files of thematically grouped rules, and use these rules throughout your project. In another folder, you can create the validator functions. You can keep your files slim and targeted using this organizational pattern.