DEV Community

Diogo Kollross
Diogo Kollross

Posted on

Dynamically required fields in Joi

Introduction

Joi is a popular JavaScript validation library. Its fluent and compact interface makes it easy to describe large, complex, validation schemas. Here's a small example:

import Joi from 'joi';

const schema = Joi.object({
  user: Joi.string().required(),
  pass: Joi.string().required(),
  address: Joi.object({
    street: Joi.string(),
    number: Joi.number(),
  }),
});

const result1 = schema.validate({ user: 'joe', pass: '123' });
// { value: { user: 'joe', pass: '123' } }

const result2 = schema.validate({ user: 'joe', pass: false });
// { error: ValidationError('"user" is required') }
Enter fullscreen mode Exit fullscreen mode

The Problem

Joi has support for conditionally changing parts of the schema based on (for example) the value or presence of some of the other object fields using the when method.

The following example requires pass only when user is present in the object:

const schema = Joi.object({
  user: Joi.string(),
  pass: Joi.string().when('user', {
    is: Joi.exist(), then: Joi.required()
  }),
});

const result1 = schema.validate({});
// { value: {} }

const result2 = schema.validate({ user: 'joe' });
// { error: ValidationError('"pass" is required') }
Enter fullscreen mode Exit fullscreen mode

But what if you want to change some field validation rules based on a condition external to the validated data? You could duplicate the schema and only change whatever is different in each case.

In this example, fields such as pass and address.street are only required when validating the payload of version 2 of our API (because they were optional in version 1):

if (apiVersion === 2) {
  schema = Joi.object({
    user: Joi.string().required(),
    pass: Joi.string().required(),
    address: Joi.object({
      street: Joi.string().required(),
      number: Joi.number(),
    }).required(),
  });
} else {
  schema = Joi.object({
    user: Joi.string().required(),
    pass: Joi.string(),
    address: Joi.object({
      street: Joi.string(),
      number: Joi.number(),
    }),
  });
};
Enter fullscreen mode Exit fullscreen mode

This works for small schemas, but it can get wild when working with huge objects wit differences scattered along several nested fields.

The Solution

I was almost giving up when I found the fork method. It creates a modified schema by running a custom callback over the schema fields you specify.

The following example is just like the previous one, but uses the fork method to modify the schema:

schema = Joi.object({
  user: Joi.string().required(),
  pass: Joi.string(),
  address: Joi.object({
    street: Joi.string(),
    number: Joi.number(),
  }),
});

if (apiVersion === 2) {
  const makeRequired = (x) => x.required();

  schema = schema.fork(
    ['pass', 'address', 'address.street'],
    makeRequired
  );
}
Enter fullscreen mode Exit fullscreen mode

I'm no specialist in Joi, but hope that this helps you. Drop a comment if you know a different way to handle this use case!

Top comments (0)

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

⭐️🎀 JavaScript Visualized: Promises & Async/Await

async await