30

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?

Nitheesh
  • 19,238
  • 3
  • 22
  • 49
  • 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 Jan 22 '20 at 14:16
  • @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 Jan 23 '20 at 06:20
  • 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 numeber – user2459780 Feb 08 '22 at 12:59
  • @user2459780 you can achieve this with `Joi.alternatives()` – Nitheesh Feb 09 '22 at 04:17

7 Answers7

37

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.

Nitheesh
  • 19,238
  • 3
  • 22
  • 49
  • 2
    Be careful when composing conditionals. There's also `any().when()`, and the difference is subtle. https://stackoverflow.com/questions/74003806/what-is-the-difference-between-alternatives-conditional-and-any-when – Ivan Rubinson Oct 09 '22 at 10:04
  • How I can handle different schemas based on types? https://stackoverflow.com/questions/74658952/joi-validations-if-object-matches-the-schema-validate-against-it-from-multiple – Bravo Dec 02 '22 at 21:32
22

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.

Shayan Shafiq
  • 1,447
  • 5
  • 18
  • 25
soamazing
  • 1,656
  • 9
  • 25
  • 32
  • Thanks. This is what worked for me. Quite clear and easy to reuse. – Misterwyz Sep 06 '21 at 10:27
  • How I can handle for different types with different schemas?https://stackoverflow.com/questions/74658952/joi-validations-if-object-matches-the-schema-validate-against-it-from-multiple – Bravo Dec 02 '22 at 21:37
  • and you could even avoid the otherwise: Joi.optional since optional is the default – Kat Lim Ruiz Jan 12 '23 at 20:12
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) }
    ])
});
Orelsanpls
  • 22,456
  • 6
  • 42
  • 69
  • 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`. – Nitheesh Jan 23 '20 at 06:02
  • @Nitheesh the [16.1.8](https://hapi.dev/family/joi/api/?v=16.1.8#alternatives) also mention of `switch` keyword. It means we are using it in a wrong way, let me look at some example I could find – Orelsanpls Jan 23 '20 at 08:07
  • your second syntax is correct. The answer that I posted is also the implementation of same in my case. Thanks :) – Nitheesh Jan 23 '20 at 08:50
  • The second one here worked perfectly for me! – Timbokun Sep 01 '22 at 05:05
  • How I can handle for different types with different schemas?https://stackoverflow.com/questions/74658952/joi-validations-if-object-matches-the-schema-validate-against-it-from-multiple – Bravo Dec 02 '22 at 21:37
8

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

orangedietc
  • 142
  • 3
  • 12
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

Alex
  • 81
  • 1
  • 8
1

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 ) },
        ],
    } ),
}
Salathiel Genese
  • 1,639
  • 2
  • 21
  • 37
  • How I can handle for different types with different schemas?https://stackoverflow.com/questions/74658952/joi-validations-if-object-matches-the-schema-validate-against-it-from-multiple – Bravo Dec 02 '22 at 21:37
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);