Joi validation - how to require or optional field based on another key exists in array?

36,427

Solution 1

You can use the Joi.when() method and create a schema like this:

Joi.object().keys({
    contact: Joi.object().keys({
        first_name: Joi.string(),
        last_name: Joi.string(),
        phone: Joi.string(),
    }),
    address: Joi.object().keys({
        place: Joi.string(),
        city: Joi.string().min(2).max(30),
        street: Joi.string(),
        house_number: Joi.string()
    }).when('contact', {
        is: Joi.object().keys({
            first_name: Joi.exist(),
            last_name: Joi.exist(),
            phone: Joi.exist(),
        }),
        then: Joi.object({ place: Joi.required() }).required(),
        otherwise: Joi.object({ place: Joi.forbidden() })
    }),
    passengers_amount: Joi.number(),
    notes: Joi.string()
});

I just simplified your schema so it's easy to understand.

Basically, what we are saying here is, if contact.first_name, contact.last_name and contact.phone exists, then address and address.place are required, otherwise address.place is forbidden.

For instance, this object will fail, because address does not exist:

{
    contact: {
        first_name: 'a',
        last_name: 'b',
        phone: 'c'
    }
}

and this will fail because address.place does not exist:

{
    contact: {
        first_name: 'a',
        last_name: 'b',
        phone: 'c'
    },
    address: {
    }
}

Finally, according to the schema defined, this object will pass:

{
    contact: {
        first_name: 'a',
        last_name: 'b',
        phone: 'c'
    },
    address: {
        place: 'd'
    }
};

Solution 2

Thanks to Soltex, thats the right schema that should be used (but please refer to the changes I have made):

Joi.object().keys({
    contact: {
      first_name: Joi.string().min(2).max(10).regex(Regex.alphabeta, 'alphabeta').allow("").error(JoiCustomErrors),
      last_name: Joi.string().min(2).max(10).regex(Regex.alphabeta, 'alphabeta').allow("").error(JoiCustomErrors),
      phone: Joi.string().min(10).max(10).regex(Regex.num, 'num').allow("").error(JoiCustomErrors),
    },
    address: Joi.object().keys({
      place: Joi.string().min(2).max(10).regex(Regex.alphanum, 'alphanum').error(JoiCustomErrors),
      city: Joi.string().min(2).max(30).required().error(JoiCustomErrors),
      street: Joi.string().min(2).max(30).regex(Regex.alphabeta, 'alphabeta').required().error(JoiCustomErrors),
      house_number: Joi.string().min(1).max(6).regex(Regex.alphanum, 'alphanum').allow("").error(JoiCustomErrors)
    }).when('contact', {
      is: Joi.object().keys({
        first_name: Joi.string().min(1),
        last_name: Joi.string().min(1),
        phone: Joi.string().min(1),
      }),
      then: Joi.object({ place: Joi.required() }).required(),
      otherwise: Joi.object({
        place: Joi.optional().allow("")
      })
    }),
    passengers_amount: Joi.number().min(0).max(4).required().error(JoiCustomErrors),
    notes: Joi.string().min(2).max(100).regex(Regex.alphanum, 'alphanum').allow("").error(JoiCustomErrors)
  })

Please note that changes from my answer to Soltex's answer: he made the when "contact.first_name" "contact.last_name" "contact.phone" to be: Joi.exists(). That is not good, since in such a way even an empty object is "exists" and then require the user to provide the "address.place". We don't want such a thing, we need at leat one char in each of those fields.

Plus, the otherwise statement in Soltex's answers is using the Joi.forbidden() while this is not the desired behaviour in here - we still need to allow the user to provide place, even without a contact, but this shouldn't be mandatory - so I used the: Joi.optional() instead.

Share:
36,427

Related videos on Youtube

Raz Buchnik
Author by

Raz Buchnik

Updated on July 09, 2022

Comments

  • Raz Buchnik
    Raz Buchnik almost 2 years

    I have this Joi schema:

      let schema = {};
    
      let stations = {
        contact: {
          first_name: Joi.string().min(2).max(10).regex(Regex.alphabeta, 'alphabeta').allow("").error(JoiCustomErrors),
          last_name: Joi.string().min(2).max(10).regex(Regex.alphabeta, 'alphabeta').allow("").error(JoiCustomErrors),
          phone: Joi.string().min(10).max(10).regex(Regex.num, 'num').allow("").error(JoiCustomErrors),
        },
        address: {
          place: Joi.string().min(2).max(10).regex(Regex.alphanum, 'alphanum').required().error(JoiCustomErrors),
          city: Joi.string().min(2).max(30).required().error(JoiCustomErrors),
          street: Joi.string().min(2).max(30).regex(Regex.alphabeta, 'alphabeta').required().error(JoiCustomErrors),
          house_number: Joi.string().min(1).max(6).regex(Regex.alphanum, 'alphanum').allow("").error(JoiCustomErrors)
        },
        passengers_amount: Joi.number().min(0).max(4).required().error(JoiCustomErrors),
        notes: Joi.string().min(2).max(100).regex(Regex.alphanum, 'alphanum').allow("").error(JoiCustomErrors)
      };
      schema.stations = Joi.array().items(stations).min(1).max(5).required().error(JoiCustomErrors);
    

    As you can see, schema.stations is an array of min 1 and max 5 elements. I want that each of the elements will have the field of "address.place" only if the "contact.first_name" AND "contact.last_name" AND "contact.phone" exists (or filled properly based on the schema).

    How can I do that?

  • Raz Buchnik
    Raz Buchnik almost 5 years
    your solution is good but it needs to be corrected. You have to adjust your schema - because in this one you have problem that when user clears the "address.place" attribute, it still an empty object and it requires it. Please see my answer, update your self to it and then I will delete mine and mark you as the correct answer.