The empty string is not considered a "valid string". This breaks a lot of validation use cases for us. The problem comes from this line.
The line was added in 32fba299 2013-11-01.
If a string cannot be empty I believe one should mark that using .min(1). Including such a restriction in the type check disallows one very valid value for the string type.
An empty string is not allowed by default because joi is used primarily to validate http input and when a query string argument has no value, all the parsers put an empty string in there. If you want to change this behavior you can just add allow('')
Its not intuitive. An empty string is by definition a string. It should be default to accept empty strings and to opt-in for rejecting them.
Agreed.
It's _surprising_ that a string fails validation for being a string (https://github.com/hapijs/joi/issues/448). It's also _surprising_ that an empty string is rejected when the string field as a whole is optional (https://github.com/hapijs/joi/issues/482).
When interfacing with strongly-typed clients or web forms, the empty field is the empty string "". As the API producer I don't have control over how or whether clients sanitize input.
It's easy to go from allowed to not allowed as the base string type and build on it.
var string = Joi.string().not("")
// use string however
It's _error prone_ to undo the constraint because I don't know whether to allow or declare empty:
// is this the right way?
Joi.string().allow("")
// or is this it?
Joi.string().empty("")
I went with empty("") thinking it would simply treat empty strings "" as undefined.
var string = Joi.string().empty("")
var schema = Joi.object().keys({
a: string.required(),
})
Joi.validate({ a: "" }, schema)
// { error: null, value: {} }
// Wut?!
It's a fantastic validation library overall, and I will continue to use it with two different string types for optional and required.
function requiredString() {
return Joi.string().required()
}
function optionalString() {
return Joi.string().empty("")
}
It's just the empty string thing is _surprising_ and _error prone_.
This issue isn't just a problem because it's surprising: it makes validation with empty strings a nightmare.
Let's say I want to have a foo param. We're using APIDoc which (like any other web form) supplies '' for any values that aren't explicitly set, so we need to allow '':
{ foo: Joi.string().allows('') }
Great, but now we want to allow either a foo or a bar param. No biggee, we just do:
Joi.object().keys({
foo: Joi.string().allows(''),
bar: Joi.string().allows(''),
}.or('foo', 'bar')
.... right? Wrong: because an empty string is now considered a true/non-empty/non-undefined value, it satisfies or, which means that the JSON { "foo": "", "bar": "" } will now pass validation when it should fail it.
The fundamental problem here is that allows means "this is a valid value". There is no way (that I'm aware of) to instead say allowsAsAnEmptyValue, and as a result you wind up writing craziness like the following just to build a Joi.alternatives that expresses "I want one of three parameters to be required":
/**
* Takes a keyVersions object and a required key and returns two Joi objects.
* The first uses the required version of the required key and the optional
* versions of all other keys (plus all optional keys), while the second
* uses just the required version of the required key (plus all optional keys).
* The two are needed to support both !x and x === '';
*
* For instance, in order to specify that at least one of a, b, or c must be
* present, and d is optional, one would need six different Joi objects:
* - a: required, b: optional, c: optional, d:optional
* - a: required, d:optional
* - b: required, a: optional, c: optional, d:optional
* - b: required, d:optional
* - c: required, a: optional, b: optional, d:optional
* - c: required, d:optional
*
* To generate the different versions one would run this function three times,
* passing in versions of:
* {
* a: [optionalA, requiredA],
* b: [optionalB, requiredB],
* c: [optionalC, requiredC],
* }
* and then 'a', 'b', or 'c' for the requiredKey, and {d: optionalD} for the
* optionalKeys.
*
* If none of the options match, an error with the provided message will be
* thrown.
*/
const makeSingleRequirementClause =
(message, keyVersions, requiredKey, optionalKeys) => [
Joi.object()
.keys(
Object.assign(
reduce(keyVersions, (newKeys, [required, optional], key) => {
// eslint-disable-next-line no-param-reassign
newKeys[key] = key === requiredKey ? required : optional;
return newKeys;
}, {}), optionalKeys)
)
.requiredKeys([requiredKey])
.error(new Error(message)),
Joi.object()
.keys(
Object.assign({ [requiredKey]: keyVersions[requiredKey][0] }, optionalKeys)
)
.requiredKeys([requiredKey])
.error(new Error(message)),
];
module.exports.requireAtLeastOneOf = (message, keyVersions, optionalKeys) => {
const clauses = map(Object.keys(keyVersions),
key =>
makeSingleRequirementClause(message, keyVersions, key, optionalKeys));
return Joi.alternatives()
.try(...flatten(clauses));
};
Great, but now we want to allow either a foo or a bar param. No biggee, we just do:
Joi.object().keys({ foo: Joi.string().allows(''), bar: Joi.string().allows(''), }.or('foo', 'bar').... right? Wrong: because an empty string is now considered a true/non-empty/non-undefined >value, it satisfies or, which means that the JSON { "foo": "", "bar": "" } will now pass validation >when it should fail it.
I think what you want is .xor. The .or() option is behaving correctly as you are using it. It looks for at least one of the peers. On the other hand, .xor() looks for exactly one of the peers.
There is no way (that I'm aware of) to instead say
allowsAsAnEmptyValue
Also, check to see if .empty('') is what you're looking for.
I think what you want is .xor.
Thanks for the response, but I don't believe xor is what I'm looking for: I'm trying to achieve "at least 1 valid parameter", not "exactly 1 valid parameter". The crazyJoi.alternatives mess I pasted was just the only way to achieve "at least 1 valid non-empty-string parameter".
Also, check to see if .empty('') is what you're looking for.
Aha, that's exactly what I needed. Thanks!
Most helpful comment
Its not intuitive. An empty string is by definition a string. It should be default to accept empty strings and to opt-in for rejecting them.