joi
joi copied to clipboard
Strip Any Value
Runtime
browser
Runtime version
125
Module version
17.13.0
Used with
standalone
Any other relevant information
No response
What problem are you trying to solve?
I'd like to have a convenient way to remove a key from an object regardless of validation status for that object. This becomes necessary when using when
, alter
, fork
, etc. Any time you want to alter an existing schema to strip a value from a schema - especially when the alteration is because you no longer care what was in it, just that it's removed.
This has been asked many times: https://github.com/hapijs/joi/issues/1385, https://github.com/hapijs/joi/issues/2372, https://github.com/hapijs/joi/issues/2533, https://github.com/hapijs/joi/issues/2423. It's been a source of confusion with no solution that I could find while searching for a few hours today.
The best approach I've come up with so far without new functionality within Joi is Joi.optional().strip().empty(Joi.any())
. This will override the key by making everything empty so that it is valid
to be stripped. However, it fails if an empty
was already applied to the key. If you need to use empty
, then you have to apply it in the non-stripped case of the conditional so that it doesn't prevent the stripping from working.
For context, what I am doing is trying to strip two keys when they should be hidden due to business logic (two separate fields with specific values). The simple case of the values of the keys being either fully valid or completely empty is simple since you can use Joi.any().strip()
. It becomes problematic when you have invalid data in the keys you wish to strip.
I've gone through several iterations of solutions as I attempted to work through the problem, and this is the most complete and least verbose one so far. Unfortunately, it is not without its issues. As mentioned, you can't use empty
outside of the when
statements, which makes it so you have to nest the when
s inside of each other in order to be able to apply an empty
when all the conditions to hide it fail. These solutions become more unwieldy/nested if you end up needing to use more fields ORed against each other to remove a key.
Joi.object({
change: Joi.object({
isImmediateRisk: Joi.boolean(),
nested: Joi.object({
critical: Joi.boolean(),
}),
}).required(),
overallImplementation: Joi.object({ value: Joi.valid(5).required() })
.when('change.isImmediateRisk', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
otherwise: Joi.when('change.nested.critical', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
otherwise: Joi.any().empty([null]),
}),
}),
});
When this is run with
{
change:{
isImmediateRisk: false,
nested: {
critical: true
}
},
overallImplementation: {}
}
it results in
{
"change": {
"isImmediateRisk": false,
"nested": {
"critical": true
}
}
}
If you just try the when
s without nesting them, then it will fail if you apply a .empty
either to the start or the end of the chain (after the .when
s). The following will fail with "overallImplementation.value" is required
, but it will work if you remove the .empty
unless the overallImplementation
value is null
, in which case it will fail if both conditions evaluate to false
, instead of being undefined
like I want it to be. (I'm using presence: 'required'
and optional
to allow saving as a draft but keeping validations of the actual data itself valid.)
Joi.object({
change: Joi.object({
isImmediateRisk: Joi.boolean(),
nested: Joi.object({
critical: Joi.boolean(),
}),
}).required(),
overallImplementation: Joi.object({ value: Joi.valid(5).required() })
.empty([null])
.when('change.isImmediateRisk', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
})
.when('change.nested.critical', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
}),
})
Similarly, using otherwise
in both when
conditions makes the stripping only work when both conditions evaluate to true
:
.when('change.isImmediateRisk', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
otherwise: Joi.any().empty([null])
})
.when('change.nested.critical', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
otherwise: Joi.any().empty([null])
})
Do you have a new or modified API suggestion to solve the problem?
- A new Symbol called something akin to
removeKey
that when merged with an object removes the key from the object - allowingstripUnknown
to work on that key due to it no-longer existing. - An
AnySchema.forceStrip()
function or an option passable toAny.strip(true, {force: true})
that could modify a schema to be removed no matter what. -
AnySchema.replace(schema)
which would either act as just theschema
, but would replace whatever it merges into with theschema
.
// This would remove the key from the resulting schema as a whole
Joi.object({toIgnore: Joi.string()}).concat(Joi.object({toIgnore: Joi.replace(undefined)}))
// This would make `toIgnore` be `Joi.any().strip()`
Joi.object({toIgnore: Joi.string()}).concat(Joi.object({toIgnore: Joi.replace(Joi.any().strip())}))
// This would make `toIgnore` be `Joi.number()`
Joi.object({toIgnore: Joi.string()}).concat(Joi.object({toIgnore: Joi.replace(Joi.number())}))
// Similarly, this would do the exact same thing - The last merged replacement would win out.
Joi.object({toIgnore: Joi.replace(Joi.string())}).concat(Joi.object({toIgnore: Joi.replace(Joi.number())}))