Vawks Clamantis

Screaming at the top of my fingers.

The Joi of Validation

Validation is like flossing: you know you should do it, but it’s easy to skip. And just like flossing, if you neglecting your validation, you’re asking for trouble.

The Joi module is Hapi’s answer to validation, but you don’t need to be using Hapi in order to use Joi. In this tutorial, we’ll show how we can use Joi to validate any object, and then we’ll take a look at how easily we can use Joi to validate requests made to Hapi applications.

Joi! Joi! Joi!

Let’s play. Save the following into joi_test.js:

1
2
3
4
5
6
7
8
9
10
11
12
var Joi = require('joi');

var schema = {
  username: Joi.string().alphanum().min(3).max(30).with('birthyear').required(),
  birthyear: Joi.number().integer().min(1900).max(2013)
};

module.exports = function(data, config) {
  config = config || {};
  var err = Joi.validate(data, schema, config);
  console.dir(err ? err : 'Valid!');
}

Check out line 10, where we call Joi.validate(data, schema, config). It matches the data against the schema and returns an error if anything’s wrong. If everything in the data object is valid according to the schema, the call returns null.

HC SVNT DRACONES: Returning null on success really is the best API, but it can lead to some counter-intuitive if statements. Be sure to test your code.

If you don’t already have a package.json file, create one with npm init, and then install the Joi module with npm install joi --save.

Now run node and follow along with the commands shown after the > prompt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> test = require('./joi_test')
[Function]
> test({})
{ details:
   [ { message: 'the value of username is not allowed to be undefined',
       path: 'username',
       type: 'any.invalid' } ],
  _object: {},
  message: 'the value of username is not allowed to be undefined' }
undefined
> test({ username: 'abc' })
{ details:
   [ { message: 'missing required peer birthyear',
       path: 'username',
       type: 'any.with.peer' } ],
  _object: { username: 'abc' },
  message: 'missing required peer birthyear' }
undefined
> test({ username: 'abc', birthyear: 1994 })
'Valid!'
undefined

Because we used .required() we have to specify the username key, and because we used .with('birthyear') we have also to include the birthyear key whenever we have a username.

The .with constraint is more typically used with non-required values. In this example, it probably makes more sense to use .required() on both username and birthyear, but we’re using a simplified version of the example in the Joi README.

Now let’s give Joi some seriously bad data:

1
2
3
4
5
6
7
8
> test({ username: 'ab', birthyear: 1894 })
{ details:
   [ { message: 'the length of username must be at least 3 characters long',
       path: 'username',
       type: 'string.min' } ],
  _object: { username: 'ab', birthyear: 1894 },
  message: 'the length of username must be at least 3 characters long' }
undefined

Even though the birthyear violates the .min(1900) constraint, we only got back an error about the username. This is because the abortEarly option defaults to true.

To make Joi send back all the errors, we use a config object with abortEarly set to true. This gets passed as the third parameter to Joi.validate(data, schema, config).

1
2
3
4
5
6
7
8
9
10
11
> test({ username: 'ab', birthyear: 1894 }, {abortEarly: false})
{ details:
   [ { message: 'the length of username must be at least 3 characters long',
       path: 'username',
       type: 'string.min' },
     { message: 'the value of birthyear must be larger than or equal to 1900',
       path: 'birthyear',
       type: 'number.min' } ],
  _object: { username: 'ab', birthyear: 1894 },
  message: 'the length of username must be at least 3 characters long. the value of birthyear must be larger than or equal to 1900' }
undefined

Check out the Joi docs to see the seven other options you can give validate.

Real Deal

The object returned by Joi.validate() a real JavaScript Error. It has a message key, and my_joi_error instanceof Error will return true.

If you want to have your program blow up on an Joi error, you can throw that object directly:

1
2
err = Joi.validate({}, {x: Joi.any().required()});
if (err) { throw err; }

If you’re using Joi in node, you’d probably use something more like this:

1
2
3
4
5
function(args, next) {
  var err = Joi.validate(value, schema, options);
  if (err) { return next(err); }
  // ...
}

Schemas Big and Small

The schema definition can be arbitrarily deep, but in the end it must terminate in a Joi schema object. Above, we saw schema objects created by Joi.string() and Joi.number().

Schema objects also have a .validate() method, and like Joi.validate() it returns either an Error object or null. But instead of taking a schema and a config, it only takes a single value.

1
2
Joi.string().validate(3);
Joi.string().validate('x');

When you use Joi.validate(), Joi effectively iterates over all the keys in your object, matching each to a corresponding schema object in your schema, and running validate(). If there are missing keys, that’s fine, unless you said they’re required().

1
2
3
Joi.validate({x: 3}, {x: Joi.string()});                       // Error
Joi.validate({}, {x: Joi.string().required()});                // Error
Joi.validate({}, {x: Joi.string()});                           // null

Extra keys will return an error, unless you set the allowUnknown option:

1
2
Joi.validate({y: 3}, {x: Joi.string()});                       // Error
Joi.validate({y: 3}, {x: Joi.string()}, {allowUnknown: true}); // null

If you’re validating an object that has keys which refer to simple data as well as keys that refer to functions, you have two choices.

First, you can explicitly set up rules that those keys should be functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var myObject = {
  count: 0,
  increment: function () {
    this.count++
  }
}

var schema = {
  count: Joi.number().integer(),
  increment: Joi.func()
}

Joi.validate(myObject, schema);
// null

Second, you could set either the allowUnknown or the skipFunctions option to true. Using skipFunctions will allow unknown keys only if they’re functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
var myObject = {
  count: 0,
  increment: function () {
    this.count++
  }
}

var schema = {
  count: Joi.number().integer()
}

Joi.validate(myObject, schema, {skipFunctions: true});
// null

Getting Specific

Joi uses a fluent interface to allow you to specify exactly what values pass the test. If you’ve used jQuery, you’ve already used a fluent interface. It lets you chain together method calls and keep getting back the same general type of object.

To start, you’ll need to call one of these factory functions: Joi.any(), Joi.array(), Joi.boolean(), Joi.date(), Joi.func(), Joi.number(), Joi.object(), Joi.string(). They do what they say on the tin, with Joi.any() accepting absolutely thing. It seems useless, but it’s worth checking out the methods defined on Joi.any() because they’re inherited by all the other schema types we’ll see later.

You can call the .allow() method to whitelist a value or array of values, regardless of the other restrictions on the schema. The .valid() method is similar, but it makes the resulting schema match only the values passed into a call to .valid().

1
2
3
4
5
6
Joi.number().allow('a').validate('a');         // null
Joi.number().allow('a').validate(3);           // null

Joi.number().valid('a').validate('a');         // null
Joi.number().valid('a').validate(3);           // Error
Joi.number().valid(3).valid('a').validate(3);  // null

That example is a bit weird, since we completely blow away the usefulness of Joi.number(), but it illustrates the difference between the two methods.

We can also use the .invalid() method to blacklist a value or an array of values. If you have multiple calls to valid and invalid, they combine and the later calls win out.

1
2
3
4
5
6
7
8
9
10
Joi.number().invalid(3).validate(3);           // Error

var schema = Joi.string().
                 valid(['a', 'b', 'c']).
                 invalid(['b', 'c']).
                 valid('c');

schema.validate('a');                          // null
schema.validate('b');                          // Error
schema.validate('c');                          // null

We’ve already seen the .required() method which means a key can’t be undefined. We can establish relationships between keys by using the .with(), .without(), .or() and .xor() methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var morning = {
  orangeJuice: Joi.any().xor('toothPaste'),
  toothPaste:  Joi.any(),
  omelette:    Joi.any().with('breakEggs').without('vegan'),
  breakEggs:   Joi.any(),
  vegan:       Joi.any()
}

Joi.validate({
  orangeJuice: true,
  toothPaste: true
}, morning);
// Error

Joi.validate({
  orangeJuice: true
}, morning);
// null

Joi.validate({
  orangeJuice: true,
  omelette: true
}, morning);
// Error

Joi.validate({
  orangeJuice: true,
  omelette: true,
  breakEggs: true
}, morning);
// null

Joi.validate({
  orangeJuice: true,
  omelette: true,
  breakEggs: true,
  vegan: true
}, morning);
// Error

The other types are fairly self explanatory Joi.array(), Joi.boolean(), Joi.date(), Joi.func(), Joi.number(), Joi.string(). They’ve all got their options, so be sure to check out the documentation.

Growing Schemas

The Joi.object() is a bit different from the others. It’s the only schema factory that can take a parameter. You can pass in a schema just like the second parameter to Joi.validate(data, schema, config). If you just call Joi.object() it will match anything where typeof thatThing is ‘object’. If you give it a schema, it will only match objects that exactly match that schema, and all the normal rules about missing keys apply.

The Joi.object() factory is a covenient way to say “I don’t want to specify my schema any deeper.” But remember it only matches objects:

1
2
3
4
5
6
obj = {x:1, y:{}}

Joi.object().validate(obj)                                   // null
Joi.object({}).validate(obj)                                 // Error
Joi.object({x: Joi.object(), y: Joi.object()}).validate(obj) // Error
Joi.object({x: Joi.number(), y: Joi.object()}).validate(obj) // null

Joi.object() is also very useful when you need reuse a matcher on different sub-objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var nameMatcher = Joi.object({
  firstName: Joi.string().required(),
  lastName: Joi.string().required()
});

var orderSchema = {
  amountCents: Joi.number().integer(),
  billingName: nameMatcher,
  shippingName: nameMatcher
}

Joi.validate({
  amountCents: 10000,
  billingName: {firstName: 'Bob', lastName: 'Dobbs'},
  shippingName: {firstName: 'Sam', lastName: 'Malone'}
}, orderSchema);
// null

Joi.validate({
  amountCents: 10000,
  billingName: {firstName: 'Bob', lastName: 'Dobbs'},
  shippingName: {firstName: 'Madonna', lastName: ''}
}, orderSchema);
// error

Hapi Hapi Joi Joi

Since Joi was created as part of the Hapi project, it’s no suprise that Joi schemas plug easily into Hapi apps. You can add a Joi schema object to a route’s config to validate the query, the payload or the path.

Here we validate the query params to make sure they’re only digits and we never include one param without the other.

We don’t even need to add the Joi module to our package.json if we’re using Hapi becuase Joi is already available as Hapi.types:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var Hapi = require('hapi');
var Joi = Hapi.types;

var server = Hapi.createServer('localhost', 8000);

server.route({
  method: 'GET',
  path: '/',
  handler: function (request, reply) {
    if (request.query.hour && request.query.minute) {
      reply(request.query.hour + ':' + request.query.minute);
    } else {
      reply('time unknown');
    }
  },
  config: {
    validate: {
      query: {
        hour: Joi.number().min(0).max(23).with('minute'),
        minute: Joi.number().min(0).max(59).with('hour')
      }
    }
  }
});

server.start();

Fire it up, and these requests will succeed:

1
2
http://localhost:8000/
http://localhost:8000/?hour=13&minute=59

But these will fail with a 400 Bad Request:

1
2
3
http://localhost:8000/?hour=13
http://localhost:8000/?hour=13&minute=99
http://localhost:8000/?hour=13&minute=nope

Easy beansy. Once again, Hapi prefers a config-oriented approach. This is a boon for testing, since you could test your entire app’s functionality with shot or PhantomJS, but you could also snag that Joi object and test it without ever having to start a server!

Conclusion

You get a lot out of Joi. Solid validation with a convenient interface. It makes it a breeze to lock down your APIs so that you only get the input you expect.