Member preview

Joi for Node: Exploring Javascript Object Schema Validation

Adopt Object Schema Validation for your NodeJS Projects

Object validation server side is a necessity for public web services — the realisation of this necessity became Joi — the most widely adopted package for object schema descriptions and validation. Joi allows us to create blueprints of Javascript objects that ensure that we process and ultimately accept accurate data. This article will explore the use cases of Joi and how to apply it to your Node projects.

Joi for the browser? No — use Yup Instead

Joi was designed for server-side scripts in mind, and in my opinion should be strictly kept on the server side. If you are looking for object validation on the front end in the browser, you can either adopt joi-browser, or an entirely different package altogether. Joi browser is the Joi community’s answer to browser-based validation, which contains the original idea of Joi combined with polyfills to ensure compatibility with ES6 standards in older browsers.

However, this adds additional size to your JS bundles and workarounds which were not considered at the fundamental conceptual level of Joi. Therefore, for the browser, Yup is a much preferred solution: the package is leaner and specifically designed for the front end in mind. It was heavily inspired by Joi and has a similar syntax model, so adopting both packages in your projects should require minimal effort.

Read more about Yup in another of my articles, focusing on usage within the React framework:

A Simple Example of Joi Schema

The idea of Joi is to define the schema of what we expect an object to resemble. For example, I have a login form on one of my apps consisting of username and password fields.

Let’s jump straight into a Joi schema object and then examine the syntax. With Joi, we can define the schema for this form in the following way:

const schema = Joi.object().keys({
username: Joi.string().alphanum().min(6).max(16).required(),
password: Joi.string().regex(/^[a-zA-Z0-9]{6,16}$/).min(6).required()
}).with('username', 'password');

This is a sample schema of my login form. Upon first inspection there may be some not so clear method calls here — in particularly keys() and with() raise eyebrows.

Let’s take a closer look at what is happening here, breaking down each piece of the syntax.

Joi .object()

I am defining Joi.object() to instantiate a Joi schema object to work with. All our schemas require Joi.object to process validation and other Joi features.

keys()

Within Joi’s keys() method, we can define the required schema constraints for our login form. Here we are defining the rules for the username and password values.

We can also define schema without using keys(), when there is only one set of keys

In the above example we are only defining one set of keys. E.g. we are only calling keys() once. In these cases, we do not actually have to call keys(), and instead define our schema directly within the object() generation, like so:

const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(16).required(),
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/).min(6).required()
}).with('username', 'password');

So why use keys() with a single key set?

To keep your code consistant. Throughout the Joi documentation keys() is used throughout, even on single key objects.

username and password schema

Within keys() we write the rules of our object schema. Here are the rules (or constraints) for our username and password values:

username: Joi.string().alphanum().min(6).max(16).required(),
password: Joi.string().regex(/^[a-zA-Z0-9{6,16}$/)

Note: The clean syntax of Joi allows us to define our schema without the need of comments or explanation of our code.

Our username must be a string, consist of alpha numeric characters only, and be between 6–16 characters in length. It is also required — we need a username to process a sign in attempt.

Joi supports regular expressions too. In the above example the regex string is defining the same rules as the above username schema: alphanumeric, between 6 and 16 characters, and therefore required.

Which style is the most beneficial — chaining methods or regex? I would argue the methods are, whereby we can read exactly what the object expects at a glance. In the case of complicated regex you may be accustomed to using in your other projects, then it makes more sense to bring it into Joi.

Let’s move onto the last line in our example, where we pass username and password into the with() method.

with(key, peers)

with() is one of many Joi methods that test schema conditions. with() takes 2 arguments, key and peers. Peers can either be a string (a single peer), or an array of peers. What with() is saying is this:

For each of these peers being present, key also must be present for the schema to be valid.

Notice that not all the peers are required to be present for the schema to be valid, but for each peer that is present, the key is also requied.

So back to our username and password example, what we are saying is:

If password is present, then username must also be present for the schema to be valid.

We also have awithout() method at our disposal, which defines which fields should not be present if a key is present!

Now, what first appeared to be a simple schema can actually warrant some head scratching until the Joi rules sink in. But this also demonstrates the power of Joi, to apply complex rules to object schema without verbose syntax we would have to resort to with vanilla Javascript.

Using keys()

As we mentioned before, keys() does not have to be used if we are defining a single key set. Additionally, if no keys are defined on a Joi.object(), then any key will be valid: there are no rules to invalidate any object we test with our Joi schema.

We also have the option to add keys after the schema’s original definition. The following example taken from the Joi documentation demonstrates this:

//define base object
const base = Joi.object().keys({
a: Joi.number(),
b: Joi.string()
});
// add a c key onto base schema
const extended = base.keys({
c: Joi.boolean()
});

As you may have noticed, we are defining constants here. Joi objects are immutable, so extending a base schema will result in a completely new object. Here we have saved that object as the constant extended.

We have also introduced the Joi.boolean() rule above, which comes in handy for testing checkboxes and other switches, where we expect a true or false value.

Some Useful Joi Schema Methods (Constraints)

So far we have used some string validation for our username and password example. Now let’s visit some other schema rules to broaden our Joi understanding.

We can also refer to these methods / rules as constraints, which is the term used throughout the Joi documentation. Our values must conform to these rules, hence constraining our values to a certain field of possible values.

email()

Email validation is pretty powerful with Joi: as well as testing the validity of the format of an email address, we can also whitelist certain top level domains. Check out the API reference on email() for the complete features.

const schema = Joi.string().email();

Numbers

We have a number of number schema constraints at our disposal with Joi. To give you a flavour of some of the options, I have selected some and have outlined some use cases:

  • greater() and less(): These methods come in useful when dealing with minimum thresholds, age or file sizes. We also have access to min() and max() for hard coded number limits.
  • integer() : Integers are important in scenarios where you require whole numbers. When requiring an explicit number of items to work with where floats would be invalid, be sure to use integer().
  • precision(): Give a certain number of decimal places to work with. Great for normalising measurements, currency.

Strings

We have already visited min, max, regex and email, but Joi has a whole bunch of other string schema constraints to work with:

  • lowercase() and uppercase(): Perhaps you are working with Enums which are case sensitive — these methods have us covered.
  • trim(): Get rid of whitespace around your strings. This actually happens when we are validating objects with our schema, which we will visit later in the article.
  • length(): Allows us to define an explicit length a string needs to be.

Dates

We can apply Joi schema keys directly to dates, with min(), max(), greater() and less() relative to time. We can also check whether an integer is a valid Unix timestamp with timestamp(), and check whether a string is a valid ISO formatted date with iso().

Arrays

To allow undefined values in arrays, we can use sparse(). We can define a strict length for an array, with length(), as well as the schema for our array items:

const schema = Joi.array().items(Joi.string(), Joi.number()); 
// array may contain strings and numbers

Objects

When working with objects, Use:

  • and() to define a relationship between key peers whereby if one is required, then they all have to be.
  • nand() to define a list of key peers whereby if one is present, then all of them together cannot be present.
  • or() is also available whereby one key peer in the list is required, and
  • xor() whereby one is required, but all of them together cannot be present.

For further investigation of objects, refer to the complete list of object constraints.

Miscellaneous Constraints of Interest

any()

Use any() to create a schema object that accepts any data type.

ip()

Joi can check for valid IP address formats, both IPV4 and IPV6. This is an easy way to validate IP addresses of your users, whether you are controlling session behaviour or preventing spam.

uri()

We can also validate a URI, consistent with the RFC 3986 standard. This is a handy tool when you only want to deal with a certain API or web service, and flag an error if not.

For a comprehensive breakdown of all the methods schema available in Joi, visit the API reference. It is worth familiarising yourself with what is available to have a full understanding of what can be applied to your projects.

Validation

Now we know how to define our schema, we need to know how to validate objects with it. We can do this with the validate() method of Joi.

I have received a username and password from a login attempt on my front end. For the sake of simplicity, let’s pass them into an object straight from my request body into an object named myObj.

To test myObj with my previously defined schema, pass it as the first argument of validate(), followed by my schema to test with:

let myObj = {
username: req.body.username,
password: req.body.password
};
const result = await Joi.validate(myObj, schema);

We can now expect a true or false result of whether our Object is valid against our schema. But there is more with the validate method — it returns a promise when we do not provide a callback method.

So we can write our validate code using async / await, or then():

//async / await
myAsyncFunction = async (value, schema) => {
const promise = await Joi.validate(value, schema);

//handle result
}
//or then()
const promise = await Joi.validate(value, schema).then(value => {
// value -> { "a" : 123 }
});

We can do a lot more with validate() with the options we have at our disposal, such as aborting early upon the first error being found in our validating schema. The entire uses are documented here.

attempt() and assert()

These are useful methods when we wish our execution to throw an error if a validation fails. attempt() will validate an object against our schema, like validate does, but throws an error if the validation fails. assert() also throws an error upon failed validation, but does not return a valid object.

Check out the signatures of these methods:

assert(value, schema, [message])
attempt(value, schema, [message])

Notice message, an optional argument allowing us to prefix our own custom error message before the validation error is displayed. This comes in handy to highlight where in your execution the error is thrown, or in what context.

describe() and compile()

Describe and compile are complimentary methods of Joi that help us “unwrap” and “wrap up” schema:

  • “unwrapped schema”: A JSON representation of our schema object. To generate such JSON, we use Joi.describe(schema).
  • “wrapped up” schema: A Joi.object() we defined above, as a Joi formatted object. We can instantiate a Joi object from a JSON schema description by using Joi.compile(schema).

Why would we want to do this? Well, JSON is a universal data format supported by a number of systems. We could easily migrate our schema rules to different apps systematically using describe and compile.

By unwrapping our schema into a JSON object we can also analyse and debug objects. We have the entire object configuration represented as JSON. Imagine defining a schema with multiple keys(), or combining multiple schemas throughout your application — describe() will be useful in these scenarios to have one representation of the entire compiled object.

Using defaults()

defaults() — supply default schema to new schemas

If we do not wish to re-define similar schema across our apps, we can use defaults() to define a default schema.The only argument is a function that returns the schema.

The documentation gives us an example use of this function, that generates a default schema based on its type, but we could also just return some schema like we have been doing:

const defaultJoi = Joi.defaults(schema => (
schema.min(1).max(20).required().integer()
));
const schema = defaultJoi.object();
//Joi.object().min(1).max(20).required().integer()

Where to go From Here

This article has highlighted the main features of Joi. You can now apply it to your projects, and have a foundation to explore the other areas of the package.

Install Joi via npm with the following command:

npm i joi

At the time of writing Joi is hitting almost 2 million weekly downloads. It is a package that is here to stay; a safe bet for your validation requirements throughout your server side Javascript projects.

Finally, refer to the Github project to explore the source code: