What I’ve Learned Validating with Joi (Object Schema Validation)
Joi is a validation library that allows you to build schemas to validate JavaScript objects. And what that generally means to me is Joi provides methods to easily validate common data types, such as e-mail addresses and phone numbers. But I’ve also learned that it allows you to easily validate less common data and complex data structures.
When validating with Joi, two pieces of data are needed:
- The data object to validate
- The Joi schema
Joi.validate(data, schema, [options]);
An optional third argument could be supplied to customise the validation behaviour but I won’t be covering that in this post.
The Basics
Validating Strings
This particular project consisted of multiple forms with general information fields — the user’s name, address, e-mail, and phone number. These fields are pretty straight forward and quite fittingly so is the validation.
Let’s take a look at an example of some data:
const data = {
name: 'Sydney Prescott',
address: '1996 Woodsboro Ln',
email: 'sydney.prescott@woodsborocc.edu',
phone: '2135551997'
};
With this general data structure, we can begin writing our Joi schema for validation. Let’s begin with the strings.
const schema = Joi.object().keys({
name: Joi.string().required(),
address: Joi.string().required(),
email: Joi.string().email().required()
});
One thing I really like about Joi is how it reads. From the schema above it is pretty clear to see what we’re expecting of each property. Additionally, by adding the required() method we can quickly organise important data while being able to double up on the required property on form fields themselves for extra security.
The address field, however, required additional consideration. While living in the States I didn’t think much about additional characters outside of our alphabet. Having moved to Finland, though, made me realise that I must now account for values that have characters such as Ää and Öö. Luckily, Joi provides additional ways to support such circumstances.
address: Joi
.string()
.trim()
.regex(/^[a-z\d\s\-\.\,]*$/i)
.max(100)
.required()
The schema above introduces a couple nice options. First, the trim() method will remove any trailing spaces from the value. The max() method is also used to limit the length of the value to 100 characters.
But the primary method I’d like to draw attention to here is regex(). This method allows greater control over what values are supported — virtually extending the default string() validation and adding custom behaviour on top.
If you had noticed, our data object stores the phone number as a string. Let’s see how we can leverage the regex() method in our schema for this case.
phone: Joi.string().trim().regex(/^[0-9]{7,10}$/).required();
By using the regex() method here, we can validate the string to ensure its value is 7–10 characters all of which fall between 0 and 9.
But you won’t always be storing phone numbers as strings. Eventually, validation for number fields will need to written and Joi makes this just as direct.
Validating Numbers
Let’s look at the following example data:
const data = {
salary: 48000,
age: 30
};
Assuming we’re looking for users between the ages of 18 and 65 our schema could look like the following:
const schema = Joi.object().keys({
salary: Joi.number().required(),
age: Joi.number().min(18).max(65).required()
});
Again, the readability of Joi is a big selling point as the use of the min() and max() methods quickly convey how we’re validating our data.
However, we won’t always have such defined ranges like 18 and 65. So how can data be validated against other data values?
Validate Against Data References
In this particular project, which deals with applying for a loan, the user is asked to specify both their total wealth as well as their total savings and investments. Because a person’s savings is part of their total wealth, the savings field should never be greater than the totalWealth value.
But since we won’t know those values until the time of validation, Joi provides the ref() method for situations exactly like this.
const data = {
totalWealth: 5000,
savings: 10000
};const schema = Joi.object().keys({
totalWealth: Joi.number().required(),
savings: Joi.number().max(Joi.ref('totalWealth')).required()
});
Inside of the max() method the value of totalWealth is referenced by using Joi’s ref() method and passing in the key of the target field. With this schema our data object would error.
Conditional Validation
There’s a fair chance you’ve either used or built a form with conditional fields. For example, why show the user fields concerning credit card debt if the user selected “No” to owning any credit cards? Our validation should follow the same logic — only validate certain fields when other criteria is met.
const data = {
hasCreditCards: true,
creditCardDebt: 750,
creditCardMonthlyPayment: 75
};
With the data model above and understanding our need for conditional validation, let’s look at our Joi schema.
const schema = Joi.object().keys({
hasCreditCards: Joi.bool().required,
creditCardDebt: Joi.number().when('hasCreditCards', {
is: true,
then: Joi.number().min(0).required()
}),
creditCardMonthlyPayment: Joi.number().when('hasCreditCards', {
is: true,
then: Joi.number().min(0).required()
})
});
In this example, the when() method is leveraged. Again, Joi reads quite clearly — when hasCreditCards is true then validate using the following schema.
But let’s say we wanted to expand our data model to include an array of credit card objects, all containing their own individual values.
const data = {
hasCreditCards: true,
allCreditCards: [{
type: 'Visa',
balance: 250,
payment: 25
}, {
type: 'Discover',
balance: 1200,
payment: 100
}]
};
In this case, we have an array with a variable number of objects that we need to validate only if the user hasCreditCards. Let’s start with what we know:
const schema = Joi.object().keys({
hasCreditCards: Joi.bool().required(),
allCreditCards: Joi.array().when('hasCreditCards', {
is: true,
then: ...
}
});
Again, using the when() method, we can conditionally validate the allCreditCards array. The next step is to validate the individual properties of each object within the array.
then: Joi.array().items({
type: Joi.string().required(),
balance: Joi.number().required(),
payment: Joi.number().required()
})
By using the items() method, we define the contents of the array. In this specific case, the items are an object but this method can support any combination of comma-separated Joi schemas.
For example, if our array would only contain a required string and an optional number our schema could look like this:
Joi.array().items(Joi.string().required(), Joi.number())
Back to our original credit card example, though. Let’s say we want to have more control over which credit card providers are supported. For example, my Discover card isn‘t accepted anywhere in Finland (like literally, anywhere!). So how could we write our validation to include and/or exclude particular values?
then: Joi.array().items({
type: Joi
.string()
.valid(['Visa', 'Mastercard'])
.invalid('Discover')
.required(),
balance: Joi.number().required(),
payment: Joi.number().required()
})
By using the valid() and invalid() methods greater control can be applied to the validation. This can be nicely paired with enum values as well.
Now, for it all together.
const schema = Joi.object().keys({
hasCreditCards: Joi.bool().required(),
allCreditCards: Joi.array().when('hasCreditCards', {
is: true,
then: Joi.array().items({
type: Joi
.string()
.valid(['Visa', 'Mastercard'])
.invalid('Discover')
.required(),
balance: Joi.number().required(),
payment: Joi.number().required()
})
})
});
This is my First Story on Medium! Consider Clapping a few times if it taught you Something!