Hapijs and Joi: validate query params with presence: 'forbidden'

zir picture zir · Oct 6, 2014 · Viewed 10.8k times · Source

My goal is to create a index route for posts. User should be able to specify some query params (ie. tags, type) but shouldn't be able to specify others. To clarify:

This is okay:

/posts
/posts?tags=food
/posts?type=regular&tags=stackoverflow

This is not okay:

/posts?title=Hello

This is hapi pack config:

servers: [
        {
            host: 'localhost',
            port: 3000,
            options: {
                labels: ["api"],
                validation: {
                    abortEarly: false,
                    presence: 'forbidden'
                }
            }
        }
    ],

Please notice the presence: forbidden option.

This is route config:

handler: function (request, reply) {
    Post.find(request.query, function (err, posts) {
        if(err) {
            console.log(err);
        }

        reply(posts);
    });
},
validate: {
    query: {
        type: Joi.string().optional(),
        tags: Joi.string().optional()
    }

}

My idea was that the validation should allow any subset of type and tags params (including empty query). However, after making any of allowed requests i'm getting a following error:

{
    "statusCode": 400,
    "error": "Bad Request",
    "message": "value is not allowed",
    "validation": {
        "source": "query",
        "keys": [
            "value"
        ]
    }
}

Why is that? There is of course no key named value. How to make the validation behave the way i want it to?

Answer

Gergo Erdosi picture Gergo Erdosi · Oct 7, 2014

If you define a non-type schema object, Joi converts it internally to an object() type. So this schema:

var schema = {
    type: Joi.string().optional(),
    tags: Joi.string().optional()
};

becomes:

var schema = Joi.object().keys({
    type: Joi.string().optional(),
    tags: Joi.string().optional()
});

Because you set presence to forbidden in server settings, it is applied to the object type, so the schema becomes:

var schema = Joi.object().forbidden().keys({
    type: Joi.string().optional(),
    tags: Joi.string().optional()
});

As you can see it marks the main object as forbidden which will not allow any value except undefined:

var Joi = require('joi');

var schema = Joi.object().forbidden().keys({
    type: Joi.string().optional(),
    tags: Joi.string().optional()
});

var value = {};

Joi.validate(value, schema, { presence: 'forbidden' }, function (err, value) {

    console.log(err);
});

It outputs:

{ [ValidationError: value is not allowed]
  name: 'ValidationError',
  details: 
   [ { message: 'value is not allowed',
       path: 'value',
       type: 'any.unknown' } ],
  _object: {},
  annotate: [Function] }

So what you need to do is to mark the main object either required or optional to override forbidden:

validate: {
    query: Joi.object().required().keys({
        type: Joi.string().optional(),
        tags: Joi.string().optional()
    })
}