Skip to content

Instantly share code, notes, and snippets.

@cb109
Last active June 5, 2023 11:30
Show Gist options
  • Save cb109/8eda798a4179dc21e46922a5fbb98be6 to your computer and use it in GitHub Desktop.
Save cb109/8eda798a4179dc21e46922a5fbb98be6 to your computer and use it in GitHub Desktop.
YUP: Validate that items in an Array match one of multiple allowed Schemas
/**
* 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)
@ddoice
Copy link

ddoice commented Sep 29, 2021

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]),
});

@ddoice
Copy link

ddoice commented Sep 29, 2021

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),
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment