-
-
Save cb109/8eda798a4179dc21e46922a5fbb98be6 to your computer and use it in GitHub Desktop.
/** | |
* The yup library has no builtin way to check if items in an array | |
* match one or more defined shapes, we can only check against a set of | |
* whitelisted values using yup.array().oneOf([..]). | |
* | |
* Using yup.addMethod() and yup.mixed().test() however we can pretty | |
* much add any custom validation we want. The function below allows to | |
* make validation pass for array items of different shapes. | |
*/ | |
const assert = require('assert'); | |
const yup = require('yup'); | |
// Please note this method is sync. An async version would return a Promise. | |
yup.addMethod(yup.array, 'oneOfSchemas', function(schemas) { | |
return this.test( | |
'one-of-schemas', | |
'Not all items in ${path} match one of the allowed schemas', | |
items => items.every(item => { | |
return schemas.some(schema => schema.isValidSync(item, {strict: true})) | |
}) | |
) | |
}) | |
// Usage example below. Run like: $ nodejs yup_array_oneOfSchemas.js | |
const Beer = yup.object().noUnknown().shape({ | |
'alcoholConcentration': yup.number(), | |
}) | |
const Water = yup.object().noUnknown().shape({ | |
'sparkling': yup.boolean(), | |
}) | |
const Crate = yup.object().noUnknown().shape({ | |
// Any item in the array must match either the Beer or Water schema. | |
'bottles': yup.array().oneOfSchemas([Beer, Water]), | |
}) | |
const crate = { | |
bottles: [ | |
{'sparkling': true}, | |
{'alcoholConcentration': 5}, | |
] | |
} | |
assert.strictEqual(Crate.isValidSync(crate, {strict: true}), true) |
In practice I found this validator to be too vague and unhelpful as it usually just errors with "the data doesn't match" without providing any helpful information - especially for end users. It's also a performance hog. It's generally better to write a validation function which knows what type to look for (eg. using duck typing or checking values of other properties). Unfortunately, that's something than cannot be written generically. I don't have an example from where I did this, but if I remember correctly, we either used the when()
and/or test()
utilities depending on the situation. The test
utility is nice because you can have dynamic error messages:
yup.object().shape({
...
foo: yup.array().test('foo', '', function(value) { // note: don't use an arrow function
if(valueIsNotFoo) {
return this.createError(...); // return a custom error (see docs)
}
return true; // everything is valid
})
...
})
@david-wb - Did you add the typescript definitions as described above? The above code works, so something else must be wrong and this is not the place to debug it. Please ask questions like this on StackOverflow, and please include code snippets so people can help.
As stated in my last comment, please ask this question on StackOverflow. This is not the place to debug issues. Since you made me type this again, just do (this as any).test(...)
. Please do not continue this discussion here.
Your mileage may vary, validating not very complex objects in node with this solution we have a throughput of 17K validations (sync) per second on a 3700X.
But we ended discarding this method because we cannot infer which is the right one in case of error to throw a meaningful error with createError.
const subschema1 = yup.object().noUnknown().shape({
type: yup.string().oneOf(['type1']).required(),
code: yup.string().min(6).required(),
number: yup.string().min(5).required(),
date: yup.string().required(),
amount: yup.number().required().positive().integer(),
field1: yup.string().url().required(),
field2: yup.object({
field3: yup.string().required(),
}),
});
const subschema2 = yup.object().noUnknown().shape({
type: yup.string().oneOf(['type2']).required(),
code: yup.string().min(6).required(),
number: yup.string().min(5).required(),
dateDelivery: yup.string().required(),
amount: yup.number().required().positive().integer(),
field1: yup.string().url().required(),
field4: yup.object({
field5: yup.string().required(),
}),
});
const schema = yup.object().noUnknown().shape({
name: yup.string().required(),
code: yup.string().required(),
services: yup.array().min(1).oneOfSchemas([subschema1, subschema2]),
});
This is how we finally did it, we created a new method called 'requiredByAssertion', which allows us to require some fields based on another field value assertion.
Following the last example:
yup.addMethod(yup.mixed, 'requiredByAssertion', function ([fields, assertion]) {
return this.when(fields, {
is: (...args) => assertion(...args),
then: (obj) => obj.required(),
});
});
const isType1 = [['type'], (type) => type === 'type1'];
const isType2 = [['type'], (type) => type === 'type2'];
const serviceSchema = yup.object().shape({
type: yup.string().oneOf(['type1','type2']).required(),
code: yup.string().min(6).required(),
number: yup.string().min(5).required(),
amount: yup.number().required().positive().integer(),
field1: yup.string().url().required(),
date: yup.string().requiredByAssertion(isType1),
dateDelivery: yup.string().requiredByAssertion(isType2),
field2: yup.object({
field3: yup.string().required(),
}).default(null).nullable().requiredByAssertion(isType1),
field4: yup.object({
field5: yup.string().required(),
}).default(null).nullable().requiredByAssertion(isType2),
});
const schema = yup.object().noUnknown().shape({
name: yup.string().required(),
code: yup.string().required(),
services: yup.array().min(1).of(serviceSchema),
});
I will just point out for others who might have stumbled on this - that this solution really does have performance problems if you have a deep or large validation tree.