Yup: How throw multiple errors in duplicate check?

Created on 29 Aug 2019  路  6Comments  路  Source: jquense/yup

This code only throw first duplicate case.
I need show all duplicated errors.

yup.addMethod(yup.array, 'unique', function(message, path) {
  return this.test('unique', message, function(list) {
    const duplicates = list
      .map(r => r[path])
      .filter((value, index, self) => self.indexOf(value) !== index);
    if (duplicates.length == 0) return true;

    // find first duplicated.
    list.forEach((row, index) => {
      if (_.includes(duplicates, row[path])) {
        throw this.createError({
          path: `${this.path}[${index}].${path}`,
          message,
        });
      }
    });

    return true;
  });
});
...
const myValidation = yup.object().shape({
  people: yup
    .array()
    .of(
      yup.object().shape(
        _.reduce(
          _.filter(Columns, col => col.validator),
          (result, col) => {
            mixin(result, {
              [col.id]: col.validator,
            });
            return result;
          },
          {},
        ),
      ),
    )
    .unique('email duplicated..', 'email')
});

I used https://github.com/jquense/yup/issues/345 as reference.

Most helpful comment

@jquense could you provide an example illustrating how this should be done?

I tried returning an array of errors, but it didn't work. I believe this happens because the returned array is treated as a "truthy" value, hence the validation is considered passed.

Throwing the array works, however it interrupts other validations despite the { abortEarly: false } option being set.

Is there any way to avoid these problems?

All 6 comments

There isn't anything special to do here, throwing always halts execution at that point, its how the language works. It's not possible to throw multiple errors at the same time. What you can do, is create and aggregate the errors into a single error and throw that, e.g. after your loop through the array.

@jquense could you provide an example illustrating how this should be done?

I tried returning an array of errors, but it didn't work. I believe this happens because the returned array is treated as a "truthy" value, hence the validation is considered passed.

Throwing the array works, however it interrupts other validations despite the { abortEarly: false } option being set.

Is there any way to avoid these problems?

@AlexLomm @kobe651jp I also want to get all the errors in an object. I think we want to use validateAt(). So if you have your values in an array, instead of doing validate(array), do a loop and save the errors, something like:

let errorsArray = []
for(let i = 0; i<array.len; i++){
  validateAt(array[i]).catch(e => {errorsArray.push(e)})
}

you will get an error per invalid field in array, the only shortcoming is that if a field in array has multiple errors, you'll only see one at a time.

@jquense is there a way to get all the errors from one type?
for example if there is yup.string().required().min(4) and the value validated is empty, is there a way to get both errors, the one thrown for required and the one thrown for min?

@Sleepful I think what you're looking for is the abortEarly flag. Example:

// ...
await someSchema.validate(data, { abortEarly: false });

Per my original question:

@jquense could you provide an example illustrating how this should be done?

I tried returning an array of errors, but it didn't work. I believe this happens because the returned array is treated as a "truthy" value, hence the validation is considered passed.

Throwing the array works, however it interrupts other validations despite the { abortEarly: false } option being set.

Is there any way to avoid these problems?

I ended up implementing a custom validation function that checks if there are any siblings with the same value in its parent array for each object. This allows us to perform checks from the objects' "perspective", rather than from the array's perspective. This is useful because it lets us have multiple errors of the same kind by "tying" them to their respective objects. Example:

import { string, array, object } from "yup";

const people = [
  { name: "John Doe", email: "[email protected]" }, // duplicate email
  { name: "Jane Smith", email: "[email protected]" },
  { name: "Jon Snow", email: "[email protected]" } // duplicate email
];

const personSchema = object({ name: string(), email: string() }).test(
  "unique",
  "Emails must be unique.",
  function validateUnique(currentPerson) {
    const otherPeople = this.parent.filter(person => person !== currentPerson);

    const isDuplicate = otherPeople.some(
      otherPerson => otherPerson.email === currentPerson.email
    );

    return isDuplicate
      ? this.createError({ path: `${this.path}.email` })
      : true;
  }
);

const peopleSchema = array().of(personSchema);

try {
  // throws an error because there are two people
  // with the same "[email protected]" email in the array
  peopleSchema.validateSync(people, { abortEarly: false });
} catch (error) {
  console.log(JSON.stringify(error, null, 2));
}

Check the CodeSandbox snippet

@AlexLomm Thanks, I ended up using this approach.
A point to consider if the input array has many entries, we might be sweeping the array multiple times while validating each entry.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

cfteric picture cfteric  路  3Comments

ScreamZ picture ScreamZ  路  4Comments

ghost picture ghost  路  4Comments

you-fail-me picture you-fail-me  路  4Comments

seanbruce picture seanbruce  路  3Comments