Joi validation - allow field to be optional but when supplied must be a positive integer
Asked Answered
T

3

8

I have a field in a JOI schema that I would like to be optional (i.e. undefined is accepted, and null is also accepted), however if a value for it is supplied, it must be a positive integer. How might I go about achieving this?

Here is what I have tried so far, with the field to validate being "capacity" however it does not seem to work, it appears the ".when" statement is just being ignored:

const divebarSchema = joi.object({
  divebar: joi
    .object({
      title: joi.string().required(),
      capacity: joi.optional().allow(null),
      description: joi.string().required(),
      location: joi.string().required(),
      image: joi.string().optional().allow(""),
      map: joi.string().optional().allow(""),
    })
    .required()
    .when(joi.object({ capacity: joi.exist() }), {
      then: joi.object({ capacity: joi.number().integer().min(0) }),
    }),
});

Before the above I originally had no .when and instead the capacity rule was:

  capacity: joi.number().optional().allow(null).integer().min(0),

However that also did not work, it kept throwing the error "must be a number" when submitting a null value.

Trangtranquada answered 9/9, 2021 at 4:9 Comment(4)
Have you tried removing the when completely, and just have capacity: joi.integer().min(0) in the original object?Deflection
@khenriksson thank for your comment. capacity: joi.integer().min(0) doesn't work as I believe .integer only works on joi.number. However capacity: joi.number().integer().min(0) isn't what I am looking for as that would not allow a null value. I already tried adding the allow(null) rule with no luck - I updated my question description to explain what I tried beforehand.Trangtranquada
try removing .required() after the joi.object()Lippe
@Lippe thanks for your comment, but I'm afraid that did not change anything, it still seems like the .while is being ignored. Also, removing .required is not what I want to do as I would like the overall object to be mandatory in the incoming request.Trangtranquada
E
5

According to Joi documentation optional does not include null you need to use allow

capacity: Joi.number().optional().integer().min(0).allow(null)
Elgar answered 9/9, 2021 at 6:12 Comment(5)
thanks for your post. Do you mean take out the .when completely, and change my capacity rule to capacity: joi.number().optional().integer().min(0).allow(null) ? This is essentially what I tried in the first place, the only difference is my .allow() was after .optional(). I tried re-ordering it to what you have suggested but unfortunately it made no difference.Trangtranquada
Correct. I tried out the rule in the joi sandbox and it worked for the scenarios {}, {capacity: null} & {capacity:3}. In fact optional seems redundant and works even when it is removedElgar
Hmm that's strange. In my app when I try what you said I still get the "must be a number" error for capacity: nullTrangtranquada
@Trangtranquada are you sure your null is not string? Did you try add .allow(null, "null")?Comeon
@Comeon thanks for your comment. I tried that and unfortunately that did not work either.Trangtranquada
L
0

You need to use any.empty rather than any.allow to make it so that null ends up considered as an empty value. This will mean that null gets stripped out of the resulting value. If you don't want null capacity to be stripped from the resulting object, you can use allow(null) as mentioned in the other answer.

I've included a snippet of code that uses both for comparison and shows the validation rules fully applying for both.

const divebarSchemaEmpty = joi.object({
  divebar: joi
    .object({
      title: joi.string().required(),
      capacity: joi.number().empty(null).integer().min(0),
      description: joi.string().required(),
      location: joi.string().required(),
      image: joi.string().allow(""),
      map: joi.string().allow(""),
    })
    .required(),
});

const divebarSchemaAllow = joi.object({
  divebar: joi
    .object({
      title: joi.string().required(),
      capacity: joi.number().allow(null).integer().min(0),
      description: joi.string().required(),
      location: joi.string().required(),
      image: joi.string().allow(""),
      map: joi.string().allow(""),
    })
    .required(),
});

const baseObject = {
  divebar: {
    title: 'capacity',
    description: 'test',
    location: 'here',
  }
};
const schemaRuns = [{
  title: 'Empty',
  schema: divebarSchemaEmpty
}, {
  title: 'Allow',
  schema: divebarSchemaAllow
}];
const runs = [{
    title: 'null capacity',
    data: {
      capacity: null
    }
  },
  {
    title: 'missing capacity',
    data: {}
  },
  {
    title: 'undefined capacity',
    data: {
      capacity: undefined
    }
  },
  {
    title: 'positive capacity',
    data: {
      capacity: 5
    }
  },
  {
    title: 'negative capacity',
    data: {
      capacity: -1
    },
    fails: true
  },
  {
    title: 'float capacity',
    data: {
      capacity: 0.25
    },
    fails: true
  }
];

for (const {
    title: baseTitle,
    data: override,
    fails
  } of runs) {
  for (const {
      title: schemaTitle,
      schema
    } of schemaRuns) {
    const title = `${schemaTitle}->${baseTitle}`;
    const data = { ...baseObject,
      divebar: { ...baseObject.divebar,
        ...override,
        title
      }
    };
    try {
      const result = joi.attempt(data, schema);
      if (fails) {
        throw new Error(`${title} succeeded validation when expected to fail`)
      }
      console.log(`${title} passed with data`, result);
    } catch (err) {
      if (joi.isError(err)) {
        if (fails) {
          console.log(`${title} passed with error object`, err)
        } else {
          // unexpected error!
          console.error(err);
        }
      } else {
        console.error(err);
      }
    }
  }
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/joi-browser.min.js"></script>
Lyell answered 9/11, 2022 at 21:51 Comment(0)
C
0

To Allow null:

capacity: Joi.string().max(255).allow(null)

To Allow empty string:

capacity: Joi.string().max(255).allow('')

To Allow both:

capacity: Joi.string().max(255).allow(null, '')
Cone answered 14/6 at 3:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.