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

8

17

I have form with dynamic amount of inputs (admin email) however checking for uniqueness fails:

      validationSchema={Yup.object().shape({
        adminEmails: Yup.array()
          .of(
            Yup.string()
              .notOneOf(Yup.ref('adminEmails'), 'E-mail is already used')

What is best approach here? FYI, as a form helper I use Formik.

Forgo answered 8/1, 2019 at 14:27 Comment(0)
W
23

Try this:

Yup.addMethod(Yup.array, 'unique', function(message, mapper = a => a) {
    return this.test('unique', message, function(list) {
        return list.length  === new Set(list.map(mapper)).size;
    });
});

Then use it like this:

const headersSchema = Yup.object().shape({
    adminEmails: Yup.array().of(
        Yup.string()
    )
    .unique('email must be unique')
})
Windburn answered 20/12, 2019 at 10:39 Comment(1)
This works, but unable to add error to the duplicate input. Any way to add error to duplicate inputs? structure: users: [{name: 'one user', age: 23}, {name: 'two user', age: 28}]. Want to add error to duplicate names.Andrien
B
8

This is a simple inline solution to validate that an array of strings only contains unique elements:

Yup.array().of(Yup.string())
.test(
  'unique',
  'Only unique values allowed.',
  (value) => value ? value.length === new Set(value)?.size : true
)
Burford answered 25/10, 2022 at 14:26 Comment(1)
Was running into issues when trying to use the .unique method with typescript using the addMethod call above. This is the much simpler solution and workd first time 👍Endocranium
D
5

If you want to have the errors in each field and not in the array

Yup.addMethod(Yup.mixed, 'uniqueIn', function (array = [], message) {
    return this.test('uniqueIn', message, function (value) {
        return array.filter(item => item === value).length < 2;
    });
});
Dodwell answered 25/5, 2022 at 5:56 Comment(1)
This answer runs array.filter for each row. See my answer below for a version that runs it only once per call to validate.Novanovaculite
H
3

Simply Do This It works for me

First Define this function in your react component

    Yup.addMethod(Yup.array, "unique", function (message, mapper = (a) => a) {
    return this.test("unique", message, function (list) {
      return list.length === new Set(list.map(mapper)).size
    })
  })

Just Put this schema inside your Formik tag

<Formik
    initialValues={{
      hotelName: "",
      hotelEmail: [""],
    }}
    validationSchema={Yup.object().shape({
      hotelName: Yup.string().required("Please enter hotel name"),
      hotelEmail: Yup.array()
        .of(
          Yup.object().shape({
            email: Yup.string()
              .email("Invalid email")
              .required("Please enter email"),
          }),
        )
        .unique("duplicate email", (a) => a.email),
    })}
    onSubmit={(values, { validate }) => {
      getFormPostData(values)
    }}
    render={({ values, errors, touched }) => (
      <Form>
            <FieldArray
                  name="hotelEmail"
                  render={(arrayHelpers) => (
                    <>
                      {values.hotelEmail.map((hotel, index) => (
                        <div class="row" key={index}>
                          <div className="col-md-8 mt-3">
                            <div className="user-title-info user-details">
                              <div className="form-group d-flex align-items-center mb-md-4 mb-3">
                                <label className="mb-0" htmlFor="hotelName">
                                  {lang("Hotelmanagement.hotelsystemadmin")}
                                  <sup className="text-danger">*</sup>
                                </label>
                                <div className="w-100">
                                  <Field
                                    name={`hotelEmail.${index}.email`}
                                    className="form-control"
                                    id="hotelEmail"
                                    placeholder={lang(
                                      "Hotelmanagement.hotelsystemadmin",
                                    )}
                                  />
                                  <span className="text-danger d-block">
                                    {errors &&
                                      errors.hotelEmail &&
                                      errors.hotelEmail[index] &&
                                      errors.hotelEmail[index].email && (
                                        <span className="text-danger d-block">
                                          {errors.hotelEmail[index].email}
                                        </span>
                                      )}
                                      {errors &&
                                      errors.hotelEmail &&(
                                        <span className="text-danger d-block">
                                          {errors.hotelEmail}
                                        </span>
                                      )}
                                  </span>
                                </div>
                              </div>
                            </div>
                          </div>
                          <div className="col-md-2 mt-3">
                            {index > 0 && (
                              <i
                                className="bx bx-minus addnewBtn "
                                onClick={() => arrayHelpers.remove(index)}
                              />
                            )}
                            {index === values.hotelEmail.length - 1 && (
                              <i
                                className="bx bx-plus addnewBtn ml-5"
                                onClick={() => arrayHelpers.push("")}
                              />
                            )}
                          </div>
                        </div>
                      ))}
                    </>
                  )}
                />



 

not to show error do this following

 {errors &&
   errors.hotelEmail &&(
      <span className="text-danger d-block">
            {errors.hotelEmail}
      </span>
 )}
)} />
Hideandseek answered 18/8, 2021 at 13:21 Comment(0)
B
2

Probably too late to respond, but anyways, you should use this.createError({path, message});

See example:

yup.addMethod(yup.array, 'growing', function(message) {
    return this.test('growing', message, function(values) {
        const len = values.length;
        for (let i = 0; i < len; i++) {
            if (i === 0) continue;
            if (values[i - 1].intervalTime > values[i].intervalTime) return this.createError({
                path: `intervals[${i}].intervalTime`,
                message: 'Should be greater than previous interval',
            });
        }
        return true;
    });
});
Beaut answered 30/12, 2020 at 0:24 Comment(1)
The links change with time, please put what necessary from that page in your answer and remove the linkGentianella
N
2

To improve the performance of Alex's answer, use something like this to pre-calculate the lookup (using lodash).

yup.addMethod(yup.mixed, 'uniqueIn', function (array = [], message) {
    return this.test('uniqueIn', message, function (value) {
        const cacheKey = 'groups';
        if (!this.options.context[cacheKey]) {
            this.options.context[cacheKey] = _.groupBy(array, x => x);
        }
        const groups = this.options.context[cacheKey];
        return _.size(groups[value]) < 2;
    });
});

then call validate with an object for context so we can store our calculated group for the duration of the validate call

schema.validate(data, {context: {}}).then(...);
Novanovaculite answered 17/11, 2022 at 16:36 Comment(0)
P
1

Improving on this answer, this works for me

import { array, ArraySchema, Flags, number, object } from 'yup';

export const yupTestUnique = <
    TIn extends any[] | null | undefined,
    TContext,
    TDefault,
    TFlags extends Flags
>(params: {
    arraySchema: ArraySchema<TIn, TContext, TDefault, TFlags>;
    iteratee?: (
        value: TIn extends readonly (infer ElementType)[] ? ElementType : never
    ) => any;
    message?: string;
}) => {
    return params.arraySchema.test(
        "unique",
        params.message ? params.message : 'must be unique',
        (values, context) => {
            return values?.length
                ? new Set(
                    values.map((value) =>
                        typeof params.iteratee === "function"
                            ? params.iteratee(value)
                            : value
                    )
                ).size === values.length
                : true;
        }
    );
};

// Example
const createProductUnitValidationSchema = object({
    unitId: number().required(),
    price: number().required().min(0),
});

const createProductValidationSchema = object({
    productUnits: yupTestUnique({
        arraySchema: array()
            .required()
            .of(createProductUnitValidationSchema)
            .min(1),
        iteratee: (value) => value.unitId,
    }),
});
Peregrine answered 28/12, 2023 at 7:25 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Hamel
S
1
import _ from "lodash";
import {addMethod, array, defaultLocale, Flags, Message, TestContext} from 'yup';
// https://github.com/jquense/yup?tab=readme-ov-file#extending-built-in-schema-with-new-methods
// https://github.com/jquense/yup/issues/345
declare module 'yup' {
  interface ArraySchema<TIn extends any[] | null | undefined, TContext, TDefault = undefined, TFlags extends Flags = ''> {
    unique(props?: UniqueProps): this;
  }

  interface ArrayLocale {
    unique?: Message<UniqueExtra>;
  }
}

export type UniqueProps = {
  /** if the item of array is object, fieldName used to associate a unique field */
  fieldName?: string,
  /** get the duplicated values */
  verbose?: boolean,
  /** custom message */
  message?: Message<UniqueExtra>,
};

export type UniqueExtra = {
  /** if {@link UniqueProps#fieldName} assigned, this will be fieldLabel */
  itemLabel: string;
  /** value join by ',' */
  rawValue?: string;
};

function rawValue(key: string, record: Record<string, number[]>) {
  return `${key}[${record[key].join(",")}]`;
}

addMethod(array, 'unique', function (props: UniqueProps = {}) {
  return this.test({
    name: "unique",
    // @ts-ignore
    message: props.message || defaultLocale.array.unique || "${path} must be unique, ${itemLabel}${rawValue} is duplicated!",
    params: {itemLabel: "value", rawValue: ""} as UniqueExtra,
    test(value: any[] | undefined, context: TestContext) {
      if (!value || value.length <= 1) return true;
      // arraySchema, itemSchema, fieldSchema
      let itemSchema = context.schema.innerType;
      const fieldName = props.fieldName;
      if (fieldName) {
        // itemSchema <- fieldSchema
        itemSchema = itemSchema.fields[fieldName];
        value = value.map((item: any) => item[fieldName]);
      }
      const itemLabel = itemSchema.spec.label;
      if (props.verbose !== true) {
        return value.length === new Set(value).size
          || context.createError({
            params: {itemLabel,},
          });
      }

      const grouped = _(value)
          .map((item, index) => ({value: item, index}))
          .groupBy('value')
          .pickBy((value, _key) => value.length > 1)
          .mapValues((value, _key) => value.map(item => item.index))
          .value()
      ;
      const keys = Object.keys(grouped);
      if (keys.length > 0) {
        return context.createError({
          params: {itemLabel, rawValue: keys.map(key => rawValue(key, grouped)).join(','),},
        });
      }
      return true;
    }
  });
});
Slowpoke answered 6/8 at 0:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.