Joi: checking against multiple values in conditional()

Published on in JavaScript

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 and b.
  • You want to validate the data with these rules:
    • a is a required string.
    • If a equals 'foo', b is a required string; otherwise b 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 and b.
  • You want to validate the data with these rules:
    • a is a required string.
    • If a is included in array, b is a required string; otherwise b 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: