Joi: checking against multiple values in conditional()
Published on in JavaScript, Joi and Testing
Last updated on
Use { is: Joi.any().valid(...values) }
to check if a value is included in an array.
Table of contents
Warm-up problem (no arrays)
- You have two values:
a
andb
. - You want to validate the data with these rules:
a
is a required string.- If
a
equals'foo'
,b
is a required string; otherwiseb
is forbidden.
Solution to the warm-up problem
Use Joi.alternatives().conditional()
to make the validation of b
depend on the value of a
:
import Joi from 'joi'
export const schema = Joi.object({
a: Joi.string().required(),
b: Joi.alternatives().conditional('a', {
is: 'foo',
then: Joi.string().required(),
otherwise: Joi.forbidden(),
}),
})
I'm using Joi 17.6.0 by the way.
Array problem
The same problem as above,
but instead of checking if a
equals 'foo'
,
we want to check if a
is included in an array:
- You have two values:
a
andb
. - You want to validate the data with these rules:
a
is a required string.- If
a
is included inarray
,b
is a required string; otherwiseb
is forbidden.
Solution to the array problem
We need to change the is
condition.
To check against multiple values,
use Joi.any().valid()
(alias: Joi.any().equal()
),
which "adds the provided values into the allowed values list
and marks them as the only valid values allowed":
import Joi from 'joi'
export const array = ['foo', 'bar', 'baz']
export const schema = Joi.object({
a: Joi.string().required(),
b: Joi.alternatives().conditional('a', {
is: Joi.any().valid(...array),
then: Joi.string().required(),
otherwise: Joi.forbidden(),
}),
})
In this example case
it's even better to use string()
instead of any()
to clarify the intent of the schema:
-is: Joi.any().valid(...array),
+is: Joi.string().valid(...array),
Unit tests
I wanted to be sure that my conditional Joi schema works, so I tested it like this using Jest:
import { array, schema } from './schema'
describe('schema', () => {
test.each(array)('requires `b` if `a` is %s', (a) => {
const validationResult = schema.validate({ a, b: undefined })
const errorMessage = validationResult.error?.details[0].message
expect(errorMessage).toBe('"b" is required')
})
test.each(array)('accepts `b` if `a` is %s', (a) => {
const validationResult = schema.validate({ a, b: 'fleebles' })
expect(validationResult).not.toHaveProperty('error')
})
it('forbids `b` if `a` is some other value', () => {
const validationResult = schema.validate({ a: 'nibbles', b: 'fleebles' })
const errorMessage = validationResult.error?.details[0].message
expect(errorMessage).toBe('"b" is not allowed')
})
it('accepts missing `b` if `a` is some other value', () => {
const validationResult = schema.validate({ a: 'nibbles' })
expect(validationResult).not.toHaveProperty('error')
})
it('accepts undefined `b` if `a` is some other value', () => {
const validationResult = schema.validate({ a: 'nibbles', b: undefined })
expect(validationResult).not.toHaveProperty('error')
})
})
Result of a test run:
$ NODE_OPTIONS=--experimental-vm-modules npx jest
PASS ./schema.test.js
schema
✓ requires `b` if `a` is foo
✓ requires `b` if `a` is bar
✓ requires `b` if `a` is baz
✓ accepts `b` if `a` is foo
✓ accepts `b` if `a` is bar
✓ accepts `b` if `a` is baz
✓ forbids `b` if `a` is some other value
✓ accepts missing `b` if `a` is some other value
✓ accepts undefined `b` if `a` is some other value
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total
Snapshots: 0 total
Time: 0.12 s, estimated 1 s
Ran all test suites.
The tests are a bit questionable:
- We depend on specific error messages. What if they change when Joi is updated?
- We are almost testing that Joi works like it should.
Should we be able to trust that
Joi.alternatives().conditional()
just works?
Nonetheless, I wanted to be sure that my schema works and these tests do the job for now.
Further resources
Joi:
Jest:
test.each()
expect().toHaveProperty()
- Using ES Modules with Jest
- You might have noticed above that I had to use
NODE_OPTIONS=--experimental-vm-modules npx jest
instead of justnpx jest
.
- You might have noticed above that I had to use