Joi validator conditional schema
Asked Answered
P

7

32

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?

Pulmotor answered 22/1, 2020 at 14:5 Comment(4)
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.Yann
@Yann 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.Pulmotor
how can I apply validation based on the type of the data like if type of data is array then check each items in array else if it is string then validation must be something else. Eg. data = [1,2,3] or data = '1'. Here if data is array then check each element is a number otherwise just check if data is numeberTappet
@Tappet you can achieve this with Joi.alternatives()Pulmotor
P
39

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.

Pulmotor answered 23/1, 2020 at 6:10 Comment(2)
Be careful when composing conditionals. There's also any().when(), and the difference is subtle. #74004306Phenomenalism
How I can handle different schemas based on types? #74659452Septivalent
B
24

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.

Begone answered 6/7, 2020 at 23:22 Comment(3)
Thanks. This is what worked for me. Quite clear and easy to reuse.Skutchan
How I can handle for different types with different schemas?#74659452Septivalent
and you could even avoid the otherwise: Joi.optional since optional is the defaultEvelineevelinn
C
9

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(),
  })
});

Cammycamomile answered 17/2, 2021 at 21:35 Comment(0)
L
8

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) }
    ])
});
Landmeier answered 22/1, 2020 at 14:13 Comment(5)
This is throwing me an error Error: "switch" can not be used with a schema condition. I'm using hapi/joi version 16.1.8.Pulmotor
@Pulmotor the 16.1.8 also mention of switch keyword. It means we are using it in a wrong way, let me look at some example I could findLandmeier
your second syntax is correct. The answer that I posted is also the implementation of same in my case. Thanks :)Pulmotor
The second one here worked perfectly for me!Carinacarinate
How I can handle for different types with different schemas?#74659452Septivalent
D
2

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

Drees answered 22/7, 2021 at 1:22 Comment(0)
B
2

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 ) },
        ],
    } ),
}
Belonging answered 25/7, 2021 at 5:35 Comment(1)
How I can handle for different types with different schemas?#74659452Septivalent
I
1

I think this solution is readable and also reflects Discriminated Unions very well.

const combination1 = Joi.object({
  type: Joi.valid(1),
  firstname: Joi.string(),
  lastname: Joi.string(),
});
const combination2 = Joi.object({
  type: Joi.valid(2),
  salary: Joi.number(),
  pension: Joi.number(),
});
const combination3 = Joi.object({
  type: Joi.valid(3),
  credit: Joi.number(),
  debit: Joi.number(),
});
const schema = Joi.alternatives().try(combination1, combination2, combination3);
Iman answered 17/2, 2023 at 4:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.