yup: mixed.oneOf doesn't respect nullable
The mixed.oneOf
doesn’t accept null
when nullable
is added.
Example
const fruitsOrNone = yup.string().oneOf(['apple', 'banana']).nullable(true)
fruitsOrNone.validate(null) // => throws ValidationError: 'this must be one the following values: apple, banana'
fruitsOrNone.validate('apple') // => apple
Expected
It’s expected to accept the value null
when nullable
is set on the schema.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 15
- Comments: 18 (3 by maintainers)
Commits related to this issue
- feat: respect nullable() with oneOf closes: #768 #104 BREAKING CHANGE: previously `oneOf` required adding `null` explicitly to allowed values when using oneOf. Folks have found this confusing and un... — committed to jquense/yup by jquense 2 years ago
- feat: respect nullable() with oneOf (#1757) closes: #768 #104 BREAKING CHANGE: previously `oneOf` required adding `null` explicitly to allowed values when using oneOf. Folks have found this confus... — committed to jquense/yup by jquense 2 years ago
Ha ha, imagine how many dev hours you’ve wasted just by being stubborn and not handling this case automatically inside the library code.
Next person in this issue can increment the counter.
yes this confusing but expected. you have a contradiction between your schema on one hand you are saying its can only be ‘apple’ or ‘banana’ and on the other say it can also be
null
, the best way to handle this is to addnull
to your oneOf configI strongly think that nullable() should add the null to the oneOf constraint automatically. Sometimes you are reusing the options passed to oneOf for your dropdowns (for example) but you don’t want a null option field added.
I’m currently doing
.oneOf([null].concat(RELATIONSHIPS), message)
or whatever fields you have in there and it looks mega weird!@jquense I could make the same argument for every schema definition that includes
.nullable()
. For instance, by usingyup.number().nullable()
you’d claim that “this is confusing” because “you have a contradiction” by saying that your value “can only be a number” and “on the other side, it says it can also benull
” (two completely different types in JS). The point is: yes, that is exactly what I’m saying it should happen.This boils down to a matter of semantics and expressing intent. I honestly wouldn’t find it confusing at all to read
yup.string().oneOf(['apple', 'banana']).nullable()
(so,"apple"
,"banana"
andnull
are all fine) - in the same way, I wouldn’t thinkyup.number().nullable()
is contradictory or unexpected. On the other hand, reading something like:…is bound to raise some eyebrows.
I agree with @nfantone and @Philipp91. The current semantics are confusing and I don’t think that consistency with Joi’s behavior is a strong argument. If Joi’s got it wrong, why not fix it?
Agree with @fnky @aphillipo and @nfantone.
IIUC,
yup.oneOf([noNulls, inHere]).nullable(true|false)
currently does not allownull
values, no matter what I pass tonullable(..)
. That is, it’s exactly equivalent toyup.oneOf([noNulls, inHere])
. And the same is true ifnull
is contained in the list of allowed values, then one wouldn’t neednullable()
on top anymore.So firstly, I don’t see how the current behavior is the most flexible approach. Unless I’m overlooking something, it makes
nullable
useless when combined withoneOf
.And secondly, it seems like changing the behavior won’t hurt anyone. The way the current behavior (and apparently also Joi’s behavior?) is, nobody would be combining these two. So these users (and consistently with Joi) can’t be negatively affected by changing the combined behavior.
I would add that
yup
claims to be composable thanks to every method returning a new yup. Because of this questionable decision, this good principle is broken.Pleeease do something about it 🙏
Here is the typing for @micktdj’s
oneOfOptional
Note that if you do this, you must put
.nullable()
before.oneOf()
otherwise TypeScript will fail with:It seems that adding
null
to the list of elements is not enough, it must also benullable
. Here’s the final working solution:It can be tested here: https://runkit.com/zhouzi/5f2d264f20ab43001a495276
Due to this non-obvious behavior, I cannot make a reusable description of the field. In a perfect world, I could do this:
And use this with native means as
field: consensus()
orfield: consensus().nullable()
.In the current implementation, I need to do crutches with passing parameters to my description.
@jquense I think this issue should be open for further discussion.
You can implement your own method with Yup.addMethod to handle this case :
Then, you can use it like this :
Finally, it works :