Joi
Created: 2018-05-19 18:42:22 -0700 Modified: 2018-06-06 10:54:51 -0700
Basics
Section titled BasicsIt’s an object validator for JS.
Validating dates/datetimes (reference)
Section titled Validating dates/datetimes (reference)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.
Adding a custom validator via “extend” (reference)
Section titled Adding a custom validator via “extend” (reference)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 file (reference)
Section titled Language file (reference)“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.
Referencing other rules, e.g. for min/max values (reference)
Section titled Referencing other rules, e.g. for min/max values (reference)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 codesHere’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).