Here's an adaptation cb109 example that runs with runkit:
var yup = require("yup")
yup.addMethod(yup.array, 'oneOfSchemas', function oneOfSchemas(
schemas,
message,
) {
return this.test(
'one-of-schemas-exact',
message || 'Not all items in ${path} match one of the allowed schemas',
(items) =>
items.every(item => schemas.some(schema => schema.isValidSync(item, { strict: true }))),
);
});
const validationSchema = yup.object().shape({
flow: yup.array().oneOfSchemas([
yup.object({
text: yup.string().required('Enter text'),
}),
yup.object({
buttons: yup.array().of(yup.string().required('Enter button title')).required(),
}),
yup.object({
input: yup.string().required(),
})
])
});
const passes = validationSchema.validateSync({
flow: [
{ text: "hello"
},
{ input: "world"
},
{ buttons: [
'hi',
'hello'
]
}
]
})
console.log(passes);
// Fails
validationSchema.validateSync({
flow: [
{ bad_input: "hello"
},
{ input: "world"
},
{ buttons: [
'hi',
'hello'
]
}
]
})
Importand gotcha: if you skip .required()
the objects will always match and the test will always return false. I.e. make sure that the objects have unique patterns and don't overlap.
For a TypeScript implementation it works poorly. I've managed to adapt the original .d.ts with something like this:
import { AnyObject, ObjectSchema } from 'yup';
// Not exported from yup
type Flags = 's' | 'd' | '';
declare module 'yup' {
export interface ArraySchema<
TIn extends any[] | null | undefined,
TContext,
TDefault = undefined,
TFlags extends Flags = ''
> {
/**
* Allows you to define mutliple disparate types which should
* be considered valid within a single array. You can tell the method
* what types of schemas to expect by passing the schema types:
*
* ```
* // Array of object schemas
* yup.array().oneOfSchemas<yup.ObjectSchema>([
* yup.object().shape({ ... })
* ]);
*
* // Array of object or string schemas
* yup.array().oneOfSchemas<yup.ObjectSchema | yup.StringSchema>([
* yup.object().shape({ ... }),
* yup.string()
* ]);
* ```
*
* @param schemas A list of yup schema definitions
* @param message The message to display when a schema is invalid
*/
oneOfSchemas<TIn1 extends AnyObject, TIn2 extends AnyObject>(
schemas: [ObjectSchema<TIn1, TContext>, ObjectSchema<TIn2, TContext>],
message?: string
): ArraySchema<(TIn1 | TIn2)[], TContext, TFlags>;
oneOfSchemas<
TIn1 extends AnyObject,
TIn2 extends AnyObject,
TIn3 extends AnyObject
>(
schemas: [
ObjectSchema<TIn1, TContext>,
ObjectSchema<TIn2, TContext>,
ObjectSchema<TIn3, TContext>
],
message?: string
): ArraySchema<(TIn1 | TIn2 | TIn3)[], TContext, TFlags>;
oneOfSchemas<
TIn1 extends AnyObject,
TIn2 extends AnyObject,
TIn3 extends AnyObject,
TIn4 extends AnyObject
>(
schemas: [
ObjectSchema<TIn1, TContext>,
ObjectSchema<TIn2, TContext>,
ObjectSchema<TIn3, TContext>,
ObjectSchema<TIn4, TContext>
],
message?: string
): ArraySchema<(TIn1 | TIn2 | TIn3 | TIn4)[], TContext, TFlags>;
// Add more variants as needed
}
}
Anyone with a nicer solution is most welcome to contribute!
The actual definition is also not the prettiest:
/* eslint-disable no-template-curly-in-string */
import { addMethod, array, MixedSchema } from 'yup';
addMethod(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
array,
'oneOfSchemas',
function oneOfSchemas(schemas: MixedSchema[], message?: string) {
return this.test(
'one-of-schemas',
message || 'Not all items in ${path} match one of the allowed schemas',
(items: unknown[]) =>
items.every((item) =>
schemas.some((schema) => schema.isValidSync(item, { strict: true }))
)
);
}
);