Creating Validation Schemas with Joi

Validating configurations, raw input and more

Fionn Kelleher
4 min readMar 27, 2014

UPDATE: This guide is extremely outdated!

I’m currently writing a book on hapi for O’Reilly called Getting Started with hapi.js. The book covers all of hapi’s main aspects, as well as a chapter devoted to joi and integrating it with hapi. I’ll be keeping it up to date with new versions of hapi, so if you’re looking to master the framework, this book could be for you. It’ll be available on O’Reilly’s website as well as Amazon in the near future.

hapi core developers have also written Developing a hapi Edge — if you’re looking to get started now, it’s worth taking a look at too!

It’s important to validate data and make sure it’s what you’re expecting it to be. I have personally seen masses of boilerplate code to check the validity of configurations, protocol messages and raw input from users. It’s nasty.

Joi allows you to clean your boilerplate code into something more pleasant, neater and easier to understand. Rather than a block of validation code, Joi allows you to actually make your validation phase human readable.

Let’s start off with an example. Let’s create a function to validate a valid TCP port — that is, an integer with the constraints of being equal or more than zero and less than or equal to 65535.

var validateTCPPort = function(port) {

// first, attempt to parse this as an integer.
port = parseInt(port);

// make sure it's actually a number, then check if valid.
if (port !== NaN && (port >= 0 && port <= 65535)) {
return null;
}
return new Error("Couldn't validate port");
}

This can be rewritten as the following using Joi:

var schema = Joi.number().integer().min(0).max(65535);
Joi.validate(6464, schema);

Neat, huh? It doesn’t stop there. As well as Numbers, Joi currently supports validation schemas for:

  • Strings
  • Arrays
  • Booleans
  • Dates
  • Functions
  • Objects

Joi also comes equipped with universal constraints that can be applied to any of the above.

Let’s use Joi.object() to create a schema to match a mock configuration for a HTTP server. Here are the constraints we’ll implement:

  • bindhost key — host/interface for the server to bind to (IPv4 or IPv6). A required key.
  • port key — TCP port (the validation we did earlier). A required key.
  • endpoints — object mapping some handlers to certain endpoints, with the root path (/) being required.
  • database key — object specifying database options. host must be a string and present in the object, name must be a string containing only [a-zA-Z0-9] and underscores, and port is an optional integer, defaulting to 5050.

Here’s how we’ll validate this via Joi:

var portSchema = Joi.number().integer().min(0).max(65535);var configSchema = Joi.object({
bindhost: Joi.string().required(),
port: portSchema.required(),
endpoints: Joi.object({
"/": Joi.string().required(),
"/customers": Joi.string().default("customersHandler").optional()
}),
database: Joi.object({
host: Joi.string().required(),
name: Joi.string().token().max(20),
port: portSchema.default(5050).optional()
})
});

We can now define a configuration as a normal JavaScript object and call on Joi to validate it against our schema.

var config = {
bindhost: "127.0.0.1",
port: 8081,
endpoints: {
"/": "rootHandler"
},
database: {
host: "192.168.0.106"
}
}
var result = Joi.validate(config, configSchema);console.log(result ? result.annotated() : null);

Running this example, “null” will be logged to stdout. Joi.validate() returns null if no errors occurred during the validation. However, let’s modify our config object to the following:

var config = {
bindhost: "127.0.0.1",
port: "this isn't a port",
endpoints: {
"/": "rootHandler"
},
database: {
host: "192.168.0.106"
}
}

The error message (formatted by Joi especially to be logged to stdout) gives us a human readable error message, as well as an annotation of our config object specifying where the error occurred.

You’ll notice that Joi halts validation after the first error, which can be very annoying behaviour when we’re wishing to correct errors quickly. To change this behaviour, we can pass an object to Joi.validate() with extra options, including one to disable “abortEarly”.

var result = Joi.validate(config, configSchema, {
abortEarly: false
});

If we run our example again we’ll be presented with every failed validation.

Joi also provides a few other configurable options; namely:

  • convert: Automatically casts values to expected types; for example, if we used this option whilst validating a TCP port in the form of a string, Joi would modify the value to a Number rather than a String.
  • allowUnknown: When set, the object can contain keys that aren’t specified in the schema, and they will be ignored.
  • skipFunctions: Only ignores unknown keys if their value is a function.
  • stripUnknown: If any keys are found that aren’t defined in the schema, they’ll be removed.
  • language/languagePath: Specify a language object or file to determine how ValidationErrors are described. Here’s the default one.

Offering An Alternative

In some cases, you may require a key in an object to be either one type or another. Let’s define a schema that allows for either a Number or an Array with maximum length 3.

var schema = Joi.alternatives([Joi.number(), Joi.array().max(3)])

Joi will pass any Number or Array (with up to 3 items) as valid.

Joi is used by Hapi to validate configuration objects (which appear everywhere), as well as to allow the definition of rules for request data.

--

--

Fionn Kelleher

17. Programmer, student, programming student. Author of “Getting Started with Hapi.js” (O’Reilly).