11
votes

I need to create dynamic schema to validate my api request query in node js using Joi validator depending on a key in the request query. Say the following below mentioned pattern are my valid queries.

I'm using hapi/joi version 16.1.8

Combination 1

{ type: 1, firstname: 'user first name', lastname: 'user last name'}

Combination 2

{ type: 2 , salary: 1000, pension: 200}

Combination 3

{ type: 3 , credit: 550, debit: 100}

As you can see the object keys varies depending on the value of type. How this can be handled properly?

We can handle two conditions using Joi.alternatives like

const schema = Joi.alternatives().conditional(Joi.object({ type: 1 }).unknown(), {
    then: Joi.object({
        type: Joi.string(),
        firstname: Joi.string(),
        lastname: Joi.string()
    }),
    otherwise: Joi.object({
        type: Joi.number(),
        salary: Joi.any(),
        pension: Joi.any()
    })
});

But how this can be done for 3 conditions?

6
I would write pre route middleware which will conditionally define which schema to use. But for Your question Grégory NEUT's answer is best fit. - num8er
@num8er I tried the solution from the documentation it was correct aswell. But when I tried to add the same an error was throwing for me. I have updated with an another working example as an answer. - Nitheesh

6 Answers

13
votes

I achieved the same in a little different manner. Posting the same here since this might be useful for someone in future.

const schema = Joi.object({
    type: Joi.number().required().valid(1, 2, 3),
    firstname: Joi.alternatives().conditional('type', { is: 1, then: Joi.string().required() }),
    lastname: Joi.alternatives().conditional('type', { is: 1, then: Joi.string().required() }),
    salary: Joi.alternatives().conditional('type', { is: 2, then: Joi.number().required() }),
    pension: Joi.alternatives().conditional('type', { is: 2, then: Joi.number().required() }),
    credit: Joi.alternatives().conditional('type', { is: 3, then: Joi.number().required() }),
    debit: Joi.alternatives().conditional('type', { is: 3, then: Joi.number().required() }),
}))

This was working perfectly as expected.

When the type value is 1 the object should have only type, firstname and lastname

When the type value is 2 the object should have only type, salary and pension

When the type value is 3 the object should have only type, credit and debit

Any other combination will be thrown as error from the joi validator middleware layer. Also any other type value other that 1, 2 and 3 will be throwing error.

7
votes

It works for me!

var Joi = require('joi');

var schema = {
    a: Joi.any().when('b', { is: 5, then: Joi.required(), otherwise: Joi.optional() }),
    b: Joi.any()
};

var thing = {
    b: 5
};
var validate = Joi.validate(thing, schema);

// returns
{
    error: null,
    value: {
        b: 5
    }
}

This is the reference.

2
votes

In the documentation it look like switch is valid key to use along alternatives.conditional. Could you try the following ?

const schema = Joi.alternatives().conditional(Joi.object({
  type: 1
}).unknown(), {
  switch: [{
    is: 1,

    then: Joi.object({
      type: Joi.string(),
      firstname: Joi.string(),
      lastname: Joi.string(),
    }),
  }, {
    is: 2,

    then: Joi.object({
      type: Joi.number(),
      salary: Joi.any(),
      pension: Joi.any(),
    }),
  }, {
    // ...
  }],
});

EDIT :

Couldn't find any example anywhere about the use of the switch keyword...

But found some other way to achieve it in hapijs/joi github

const schema = Joi.object({
     a: Joi.number().required(),
     b: Joi.alternatives()
             .conditional('a', [
                 { is: 0, then: Joi.valid(1) },
                 { is: 1, then: Joi.valid(2) },
                 { is: 2, then: Joi.valid(3), otherwise: Joi.valid(4) }
    ])
});
2
votes

I was trying to find a way to do something similar. Then I was able to figure it out.

const Joi = require('joi');
const schema = Joi.object({
  type: Joi.number().valid(1,2,3),
  // anything common
}).when(Joi.object({type: Joi.number().valid(1)}).unknown(), {
  then: Joi.object({
    firstname: Joi.string(),
    lastname: Joi.string(),
  })
})
.when(Joi.object({type: Joi.number().valid(2)}).unknown(), {
  then: Joi.object({
    salary: Joi.number(),
    pension: Joi.number(),
  })
})
.when(Joi.object({type: Joi.number().valid(3)}).unknown(), {
  then: Joi.object({
    credit: Joi.number(),
    debit: Joi.number(),
  })
});

1
votes

I was looking to do the same but instead of query param the condition should depend on the request method. I ended up with the next solution:

const schema = Joi.when(Joi.ref("$requestMethod"), {
  switch: [
    {
      is: "POST",
      then: Joi.object({
        name: Joi.string().trim().max(150).required(),
        description: Joi.string().max(255),
        active: Joi.boolean().required(),
        }),
      }),
    },
    {
      is: "PUT",
      then: Joi.object({
        name: Joi.string().trim().max(150).required(),
        description: Joi.string().max(255),            
      }),
    },
  ],
});


schema.validate(req.body, { context: { requestMethod: req.method } });

Joi when() condition documentation https://joi.dev/api/?v=17.4.1#anywhencondition-options

0
votes

Using the conditional/switch, of which syntax is also more readable.

The following Joi excerpt will enforce the presence of $.referenceClass when $.type === Type.REFERENCE. When it does, it will ensure that values are the accepted ones - ...classIds.

Otherwise ($.type !== Type.REFERENCE), it won't allow the presence of $.referenceClass (except you run the validation with the option of allowing extra keys).

{
    type: Joi.string().valid( ...Hub.types ).required(),
    referenceClass: Joi.alternatives().conditional( 'type', {
        switch: [
            { is: Type.REFERENCE, then: Joi.string().valid( ...classIds ) },
        ],
    } ),
}