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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
If you’re using Joi in node, you’d probably use something more like this:
1 2 3 4 5 |
|
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 |
|
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 |
|
Extra keys will return an error, unless you set the allowUnknown
option:
1 2 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
Fire it up, and these requests will succeed:
1 2 |
|
But these will fail with a 400 Bad Request:
1 2 3 |
|
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.