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)
@DesignByOnyx
Copy link

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.

@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