How to test for uniqueness of value in Yup.array?

Imagine you have a form with a dynamic amount of inputs – in my case, it is an array of objects holding “name” and “surname” properties and you need to validate that all objects in this array have unique name property.

Example schema:

const users = yup.array().of(yup.object().shape({
    name:  yup.string(),
    surname: yup.string(),
}))

How can you test for uniqueness? There is no built-in validator, but you can create a custom one easily extending Yup.test.  This validator allows you to add a test function to the validation chain. Tests are run after any object is cast. In order to allow asynchronous custom validations all (or no) tests are run asynchronously. A consequence of this is that test execution order cannot be guaranteed.

All tests must provide a name, an error message and a validation function that must return true when the current value is valid and false or a ValidationError otherwise. To make a test async return a promise that resolves true or false or a ValidationError.

For the message argument you can provide a string which will interpolate certain values if specified using the ${param} syntax. By default, all test messages are passed a path value which is valuable in nested schemas.

The test function is called with the current value.

const users = yup.array().of(yup.object().shape({
    name:  yup.string(),
    surname: yup.string(),
})).test(
    'unique',
        t('step2.validation_errors.duplicate').toString(),
        (value) => {
          if (!value) return true;

          const unique = value.filter((v: any, i: number, a: any) => a.findIndex((t: any) => (t.name === v.name && t.surname === v.surname)) === i);
          return unique.length === value.length;
    }
)

Because the uniqueness check is not related to a particular element of the users object but is a general validation constraint for the whole object, the error message will be populated as “errors.people” property of Formik. That’s why I am checking if the erros.users is a string – if it is a string, I should display a general error above all the user’s fields, but if it is an array of errors – it contains errors for a particular index of the users array.

{typeof errors.users === 'string' && <ErrorMessage
name="users"
component="div"
className="elementor-field-group mt-2 text-sm text-red-600 dark:text-red-500"
/>}