Skip to content

Joi

Created: 2018-05-19 18:42:22 -0700 Modified: 2018-06-06 10:54:51 -0700

It’s an object validator for JS.

Joi has relatively basic support for this; there’s only a built-in way to verify that dates or datetimes are specified, not just datetimes:

const schema = Joi.object()
.required()
.keys({
startTime: Joi.string().isoDate().options({convert: true}),
});

(note: “convert: true” is on by default, but it doesn’t hurt to explicitly say this here in case you have “strict()” somewhere else (since “strict” will disable “convert” by default))

This will take in something like ‘2018-12-25’ and convert it to “2018-12-25T00:00:00.000Z”. It will also that same output string as input and do nothing to it.

Note that “convert” here does not mean to change it into a Date object; it’s still a string, it’s just a string in the ISO format.

If it’s important to only allow datetimes (and not just dates), then you can use this extension with a custom format: https://github.com/hapijs/joi-date-extensions

Building up rules from a “base”

Section titled Building up rules from a “base”

(keywords: clone, extend, add)

Scenario: you have something like “arrayOfUserIds”, then you also want “arrayOfUniqueUserIds” that simply adds the “unique” flag to the first array:

export const arrayOfUserIds = Joi.array()
.min(1)
.items(Joi.number().integer());
export const arrayOfUniqueUserIds = arrayOfUserIds.unique();

I looked into the code of Joi to figure out what’s happening here, and it turns out there’s a call to an internal function named “clone”, so you don’t have to do anything extra:

_test(name, arg, func, options) {
const obj = this.clone();
obj._tests.push({ func, name, arg, options });
return obj;
}

This ensures that you’ll get back two completely different schemas (which you can verify via “describe” below):

JSON.stringify(arrayOfUserIds.describe());
JSON.stringify(arrayOfUniqueUserIds.describe());
arrayOfUserIds: {
"type": "array",
"flags": {
"sparse": false
},
"rules": [
{
"name": "min",
"arg": 1
}
],
"items": [
{
"type": "number",
"invalids": [
null,
null
],
"rules": [
{
"name": "integer"
}
]
}
]
}
arrayOfUniqueUserIds: {
"type": "array",
"flags": {
"sparse": false
},
"rules": [
{
"name": "min",
"arg": 1
},
{
"name": "unique",
"arg": {}
}
],
"items": [
{
"type": "number",
"invalids": [
null,
null
],
"rules": [
{
"name": "integer"
}
]
}
]
}

As shown above in small text, the unique schema has an extra rule.

This is possible via “extend”. An example case that I ran into was when I was validating a username; I wanted to disallow in a case-insensitive way against a blacklist. It turns out there’s already a way to do this (“.insensitive().disallow(NAMES)”), but if I ever do want a custom rule so that I could just say “.noBlacklistedNames”, I could use “extend”.

“language” is an option that you can pass in to validate that will localize/format the error messages. I haven’t actually used this yet.

The reference covers this (bolded below):

const leagueObjectForSeason = Joi.object()
.strict()
.keys({
minRating: Joi.number()
.integer()
.required(),
maxRating: Joi.number()
.greater(Joi.ref('minRating'))
.integer()
.required(),
});

Converting Joi errors into application-specific error codes

Section titled Converting Joi errors into application-specific error codes

Here’s some sample code to handle converting multiple errors into specific codes you may have:

const _ = require('lodash');
function handleErrorsWithErrorCodes(errorCodeMappings, errors) {
const typesOfErrors = _.map(errors, "type");
const errorObjects = _.map(typesOfErrors, typeOfError => {
return {
type: _.get(errorCodeMappings, typeOfError, errorCodeMappings.default)
};
});
return errorObjects;
}
// This assumes you have "ErrorCodes" defined somewhere.
const usernameErrors = {
"string.min": ErrorCodes.MIN,
"string.max": ErrorCodes.MAX,
"string.alphanum": ErrorCodes.ALPHANUM,
default: ErrorCodes.default
};
const usernameValidator = Joi.string()
.min(3)
.max(15)
.alphanum()
.error(handleErrorsWithErrorCodes.bind(null, usernameErrors))
// We have to prevent aborting early so that we get all possible errors.
.options({ abortEarly: false });
const result = Joi.validate(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(((",
usernameValidator
);

After running this code, you’ll get the following result:

result: {
"error": {
"isJoi": true,
"name": "ValidationError",
"details": [
{
"message": "Error code \MAX\ is not defined, your custom type is missing the correct language definition",
"path": [],
"type": "MAX",
"context": {
"label": "value"
}
},
{
"message": "Error code \ALPHANUM\ is not defined, your custom type is missing the correct language definition",
"path": [],
"type": "ALPHANUM",
"context": {
"label": "value"
}
}
],
"_object": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(("
},
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(("
}

Note: the messages that you see are a result of the error’s toString() function not being able to find a language object (see this note).