Joi: options in nested schemas?

Created on 22 Jul 2018  路  11Comments  路  Source: sideway/joi


I'd like to know if there's a way to not "allowUnknown" keys in a nested object of a parent that allows allowUnknown, currently I have solved but separating the validations, it would be nice to add this option to the nested object and during the compilation/validation time it knows that the nested won't accept unknown keys.


This is an example of how I'm doing, maybe you could give me a better idea:

src/validations/index.js (request validation)

export default function requestValidator(req) {
  let result = Joi
    .object()
    .label('req')
    .empty()
    .required()
    .error(e => `Invalid parameters`)
    .validate(req, { abortEarly: false, allowUnknown: true })
  return errorsAsPromise(result, 'req')
}

src/validations/Tenant.js (input validation)

export function createMany(body) {
  let result = Joi.object({
    data: Joi.array().items(Joi.object({
      id: id.forbidden(),
      firstname: firstname.required(),
      lastname: lastname.required(),
      house: house.required(),
      created_at: createdAt,
      updated_at: updatedAt,
      deleted_at: deletedAt
    }).empty()).single().empty().label('data').required(),
    options,
  })
  .label('body')
  .empty()
  .required()
  .error(e => `Invalid parameters`)
  .validate(body, { abortEarly: false, allowUnknown: false })
  return errorsAsPromise(result, 'createMany')
}

This would be the desired result:

export function createMany(req) {
  let result = Joi
    .object({
      body: Joi.object({ // allowUnknown: false? how?
        data: Joi.array().items(Joi.object({
          id: id.forbidden(),
          firstname: firstname.required(),
          lastname: lastname.required(),
          house: house.required(),
          created_at: createdAt,
          updated_at: updatedAt,
          deleted_at: deletedAt
        }).empty()).single().empty().label('data').required(),
        options,
      }).label('body').empty().required(),
      params: Joi.object().label('params'),
      query: Joi.object().label('query')
    })
    .label('req')
    .empty()
    .required()
    .error(e => `Invalid parameters`)
    .validate(req, { abortEarly: false, allowUnknown: true })
  return errorsAsPromise(result, 'createMany')
}
/* 
 * The problem with this approach is that when the parent has set to true 
 * the option: "allowUnknown: true" the children also allows unknown keys 
 * and I don't want to allow more keys in nested objects/schemas
 */

Currently I have separated the validations, they are used as middlewares in my routes but it would be nice to not to add a middleware to my routes when I can achieve the same but inside a single validation, (i mean request validation + input validation), they could be achieved in a single validation.

Are you ready to work on a pull request if your suggestion is accepted?

Yes

non issue

All 11 comments

Sorry but the option: unknow(false) solved my problem 馃憤
Closed.

No problem. Just a comment though, I see you're using empty() without any argument all the time, why ? It serves no purpose.

Also, you really shouldn't be creating your schemas every time a route is called, creation is slow.

Hi @Marsup let me answer you this:
"I see you're using empty() without any argument all the time, why ? It serves no purpose."

Well "empty" is a custom extension that I have created to check emptiness for objects & arrays due to the "empty" method of the docs doesn't check properly empty arrays or objects well, only in strings does well, it says "any.empty()" but at the end if I use a schema like this:

let result = Joi
    .array()
    .items(Joi.object({ name: Joi.string() }))
    .empty()
    .validate(req)

Then if I execute it:

validations.createMany([])
/*The validation passes as valid, and it must throw an error because the requirements says that an empty array is not allowed*/

So, the example definition of the extension is the following:

export function empty(type = 'object') {
  return function(joi) {
    return {
      name: type,
      base: joi[type](),
      language: {
        empty: 'must not be empty'
      },
      rules: [{
        name: 'empty',
        validate(params, value, state, options) {
          if (is.empty(value)) {
            return this.createError(`${type}.empty`, { value }, state, options)
          }
          return value
        }
      }]
    }
  }
}

Then I set it:

export const Joi = Base
  .extend(empty('object'))
  .extend(empty('array'))

export function errorsAsPromise(result, path) {
  if (result.error) {
    return Promise.reject(
      new ValidationError(
        result.error.message, {
          errors: result.error.details.map(error => error.message),
          path,
          validator: result
      })
    )
  }
  return Promise.resolve(true)
}

Now the error is thrown properly and my tests are working as I expected.

@Marsup "Also, you really shouldn't be creating your schemas every time a route is called, creation is slow."

mmm, that might be true coming from you because you're one of the creators/members of this tool, I started to use it because I wanted to have a good validator for my architecture, let me tell you why I have chosen Joi:

  1. It's able to give us an array of errors, not only stop in the first error found.
  2. I'm able to create my own errors and wrap them into a single unit to let the client app know how many inputs failed in an action performed.
  3. It fits well in my architecture due to that I have schemas of entities, like MongoDB has it's own, I can also have the schemas for other databases like MySQL.
  4. The team understand the validations easily, they don't have to think about the if else if nested that in some cases are really ugly.

This is an idea of how is the flow of a request, but I'm just gonna focus in the validation part:

request-diagram-flow
_There're a lot of details ignored here, but this is just an idea of the architecture._

As you can see in the first block yellow they are separated in two sections, schemas and handlers.

Validation for schemas: in this I only have typical fields in a entity like id, firstname, etc... all of them are prepared with Joi, just to call them in different places and also make them required or optional in different handlers.
example:

export const id = Joi
  .number()
  .integer()
  .positive()
  .label('id')

export const house = Joi
  .number()
  .min(1)
  .max(1000)
  .integer()
  .positive()
  .label('house')

export const request = Joi
  .object()
  .empty()
  .required()
  .options({ abortEarly: false, allowUnknown: true })
  .label('req')
  .error(e => `Invalid parameters`)

Validations for actions/handlers/services (synonyms): here I test the structure of particular request and the fields/schema of an entity too, for example, in the body can come an object of "fields" that are going to be updated and another key sibbling in the same root level for options.
In this layer I take the schemas or part of them of my entities using joi and set as required in one handler and in others mark them as optionals.
example:

export function readMany(req) {
  let result = request.keys({
    query: Joi.object({
      options: options.unknown(false),
      search: Joi.string().empty('').label('search'),
      fields: Joi.object({
        id,
        firstname: Joi.string().empty('').label('firstname'),
        lastname: Joi.string().empty('').label('lastname'),
        house,
        created_at: Joi.object(dateRangeAt).empty().label('created_at'),
        updated_at: Joi.object(dateRangeAt).empty().label('updated_at'),
        deleted_at: Joi.object(dateRangeAt).empty().label('deleted_at'),
      }).unknown(false).label('fields'),
    }).unknown(false).label('query')
  }).validate(req)

  return errorsAsPromise(result, 'readMany')
}

As you could see the validation structure of the layer is a requirement here, I have the project working well with the old validations using if else if etc, but Joi fits really well.

Now the performance is something that I haven't contemplated, and it's true because with if else if I'm not creating objects, I will think on it, luckily I'm working in a branch of the codebase and this is being tested, after all the validations has passed all the unit test, I will do integration test to check issues with the performance.

Could you suggest me something to take in consideration?
And if so, Why? because I don't know how to have this compiled outside of the routes.

Thanks.

Well "empty" is a custom extension that I have created to check emptiness for objects & arrays

You can already do min(1) on both arrays and objects for that. Overriding a native method with your own completely different implementation is not only going to be confusing for people working with you but also prevent you from using the native one when you need to, you probably shouldn't name it the same if the behavior has nothing to do with it.

Could you suggest me something to take in consideration?
And if so, Why? because I don't know how to have this compiled outside of the routes.

Depending on the server you're using, there may already be tools to help you with that. If you're not using any framework, it's only standard js, so store it somewhere and re-use it, just like you did with parts of the schemas but for complete ones.

You can already do min(1) on both arrays and objects for that

I didn't know that, thanks.

Depending on the server you're using

I'm using JS with Node + Express, but the same Idea can be applied to a server with Koa or Hapi

store it somewhere and re-use it, just like you did with parts of the schemas but for complete ones.

Thanks I appreciate it, it gives me a better idea to fix the issues you mentioned.

Sorry but I just reopened it to see if you have something to say about this answer:

I'm using JS with Node + Express, but the same Idea can be applied to a server with Koa or Hapi

There are many modules out there, I know https://www.npmjs.com/package/celebrate works well with Express and is from a friend, take time to compare your options.

@Marsup Many thanks!!! it will reduces a lot of boilerplate, even I have some ideas to abstract the job, I really appreciate it, the team will be glad to see this package as we will be removing some amount of code, thanks.

Closed.

This thread has been automatically locked due to inactivity. Please open a new issue for related bugs or questions following the new issue template instructions.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

chrisegner picture chrisegner  路  4Comments

ashrafkm picture ashrafkm  路  3Comments

Taxi4you picture Taxi4you  路  3Comments

n-sviridenko picture n-sviridenko  路  3Comments

kailashyogeshwar85 picture kailashyogeshwar85  路  4Comments