Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to apply different validation to each element of an array? #528

Closed
otori23 opened this issue May 1, 2019 · 17 comments
Closed

How to apply different validation to each element of an array? #528

otori23 opened this issue May 1, 2019 · 17 comments

Comments

@otori23
Copy link

otori23 commented May 1, 2019

I have an array of three numbers: [min, mid, max], and I want to apply different validation function(s) to each element of the array. I don’t quite know how to achieve this with Yup.

Example use cases:

  1. min: no validation; mid: validate that mid > min; max: validate that max > mid

-or-

  1. min: required; mid: not required; max: required

I know something like array().of(number()…) will apply the same validation to each element in the array, but this is not what I want. Each element in the array has its own validation requirements.

Thanks.

@eddyw
Copy link

eddyw commented May 20, 2019

Not sure if it'd work but, did you try treating the array as an object?
Something like:

const schema = object().shape({
  0: string(), 
  1: number(),
  2: boolean(),
  length: number()
})

@jquense
Copy link
Owner

jquense commented May 20, 2019

What you seem to be looking for is basic tuple support, which isn't what array is meant to handle. I'd recommend generally that you turn your tuple into an object like {min, mid, max} if you can, since you'd be able to define individual schema for each. Otherwise you could create a custom tuple schema type that extends array but has a different signature.

@otori23
Copy link
Author

otori23 commented May 28, 2019

@eddyw thanks for the suggestion. I tried it out but did not get the behavior that I was looking for.

@otori23
Copy link
Author

otori23 commented May 28, 2019

@jquense I am actually using Yup in the context of Formik validation. I was able to implement the behavior I was after using Yup's mixed.test() API. So the validation schema looks something like:

const schema = Yup.object().shape({
  values: Yup.array(),
  'values[0]': Yup.number().test('test-0', 'test-msg', function() {
    // test for values[0]
    return this.parent && this.parent.values && this.parent.values[0] !== undefined
  }),
  'values[1]': Yup.number().test('test-1', 'test-msg', function() {
    // test for values[1]
    return this.parent && this.parent.values && this.parent.values[1] > this.parent.values[0]
  }),
  'values[2]': Yup.number().test('test-2', 'test-msg', function() {
    // test for values[2]
    return this.parent && this.parent.values && this.parent.values[2] > this.parent.values[1]
  })
})

Basically, in test(...), I can get at the individual array values, and implement custom test logic.

Thanks for the help.

@Clement-Bresson
Copy link

@otori23 is there a mistake in your example ?
In need to handle a very similar case as yours but cant make it work.

 values: Yup.array(), <=== HERE
  'values[0]': Yup.number().test('test-0', 'test-msg', function() {

Thank you so much

@jquense jquense closed this as completed Jan 16, 2020
@ilomon10
Copy link

ilomon10 commented Feb 14, 2020

@jquense I am actually using Yup in the context of Formik validation. I was able to implement the behavior I was after using Yup's mixed.test() API. So the validation schema looks something like:

const schema = Yup.object().shape({
  values: Yup.array(),
  'values[0]': Yup.number().test('test-0', 'test-msg', function() {
    // test for values[0]
    return this.parent && this.parent.values && this.parent.values[0] !== undefined
  }),
  'values[1]': Yup.number().test('test-1', 'test-msg', function() {
    // test for values[1]
    return this.parent && this.parent.values && this.parent.values[1] > this.parent.values[0]
  }),
  'values[2]': Yup.number().test('test-2', 'test-msg', function() {
    // test for values[2]
    return this.parent && this.parent.values && this.parent.values[2] > this.parent.values[1]
  })
})

Basically, in test(...), I can get at the individual array values, and implement custom test logic.

Thanks for the help.

how about target path to last index
any example for lastIndex

maybe

const schema = Yup.object().shape({
  values: Yup.array(),
  'values[0]': Yup.number(),
  'values[lastIndex]': Yup.string()
})

@Hoxtygen
Copy link

@jquense I am actually using Yup in the context of Formik validation. I was able to implement the behavior I was after using Yup's mixed.test() API. So the validation schema looks something like:

const schema = Yup.object().shape({
  values: Yup.array(),
  'values[0]': Yup.number().test('test-0', 'test-msg', function() {
    // test for values[0]
    return this.parent && this.parent.values && this.parent.values[0] !== undefined
  }),
  'values[1]': Yup.number().test('test-1', 'test-msg', function() {
    // test for values[1]
    return this.parent && this.parent.values && this.parent.values[1] > this.parent.values[0]
  }),
  'values[2]': Yup.number().test('test-2', 'test-msg', function() {
    // test for values[2]
    return this.parent && this.parent.values && this.parent.values[2] > this.parent.values[1]
  })
})

Basically, in test(...), I can get at the individual array values, and implement custom test logic.

Thanks for the help.

I'm using this approach to validate file uploads but I'm having issues.
More context here:
I'm using a single input field to upload many files using the multiple attribute. I want to validate each image file that it is of a certain type and size. Using your approach it works but with a big gotcha.

The minimum amount of file expected is one and a maximum of five. This means I have to write validations for the maximum number of files. Things seem to not work out if the maximum number of files is not uploaded; it returns an error.

For example

images: Yup.array().min(1).max(5).required("At least a file is required"),
    "images[0]": Yup.mixed().test("fileSize", "File too large", function () {
      return this.parent && this.parent.images &&  this.parent.images[0] && this.parent.images[0].size <= FILE_SIZE;
    })
    .test("fileFormat", "Your file format must be jpg, png, and jpeg", function () {
      return this.parent && this.parent.images &&  this.parent.images[0] && SUPPORTED_FORMATS.includes(this.parent.images[0].type);
    }),
    
    "images[1]": Yup.mixed().test("fileSize", "File too large", function () {
      return this.parent && this.parent.images &&  this.parent.images[1] && this.parent.images[1].size <= FILE_SIZE;
    })
    .test("fileFormat", "Your file format must be jpg, png, and jpeg", function () {
      return this.parent && this.parent.images &&  this.parent.images[1] && SUPPORTED_FORMATS.includes(this.parent.images[1].type);
    }),

If only one file is uploaded and meet the criteria , errors for the second file is thrown. What do I need to do to make sure that validation only runs for the number of uploaded files.?

@javierfuentesm
Copy link

@otori23 is there a mistake in your example ?
In need to handle a very similar case as yours but cant make it work.

 values: Yup.array(), <=== HERE
  'values[0]': Yup.number().test('test-0', 'test-msg', function() {

Thank you so much

did you solved it? I can not make it work neither :(

@philly-vanilly
Copy link

philly-vanilly commented Apr 26, 2021

Why was this closed? The above solutions do not work in Typescript.

@philly-vanilly
Copy link

What you seem to be looking for is basic tuple support, which isn't what array is meant to handle. I'd recommend generally that you turn your tuple into an object like {min, mid, max} if you can, since you'd be able to define individual schema for each. Otherwise you could create a custom tuple schema type that extends array but has a different signature.

tuples are used by other frameworks though, like date/calendar inputs and adding an extra state to fit the validation framework even though the ui framework manages the state just fine is kinda smelly

@joematune
Copy link

I had luck adding a new method to Yup.array() ~> Yup.array().tuple(). You can then define a schema with a tuple and validate a tuple - the new method takes care of the transformation.

Yup.addMethod(Yup.array, 'tuple', function(schema) {
  if (!this.isType(schema)) Yup.ValidationError();
  return Yup.object({
    tuple: Yup.array().min(schema.length).max(schema.length),
    ...Object.fromEntries(Object.entries(schema)),
  }).transform((value, originalValue) => {
    if (!this.isType(originalValue)) Yup.ValidationError();
    return {
      tuple: originalValue,
      ...Object.fromEntries(Object.entries(originalValue))
    };
  });
});

// Create your schema
const MySchema = Yup.array().tuple([
  Yup.number(),
  Yup.string(),
  Yup.boolean(),
]);

// Validate your tuple
const { tuple } = await MySchema.validate([1999, "ja", true]);

console.log(tuple); // [1999, "ja", true]

Check it out on RunKit - [here]

@brandoncroberts
Copy link

fyi tuple has been released within the @next version of yup.
Was just about to try to test the snippet from @joematune, and then saw that.

@ijunaid8989
Copy link

I had luck adding a new method to Yup.array() ~> Yup.array().tuple(). You can then define a schema with a tuple and validate a tuple - the new method takes care of the transformation.

Yup.addMethod(Yup.array, 'tuple', function(schema) {
  if (!this.isType(schema)) Yup.ValidationError();
  return Yup.object({
    tuple: Yup.array().min(schema.length).max(schema.length),
    ...Object.fromEntries(Object.entries(schema)),
  }).transform((value, originalValue) => {
    if (!this.isType(originalValue)) Yup.ValidationError();
    return {
      tuple: originalValue,
      ...Object.fromEntries(Object.entries(originalValue))
    };
  });
});

// Create your schema
const MySchema = Yup.array().tuple([
  Yup.number(),
  Yup.string(),
  Yup.boolean(),
]);

// Validate your tuple
const { tuple } = await MySchema.validate([1999, "ja", true]);

console.log(tuple); // [1999, "ja", true]

Check it out on RunKit - [here]

Hi thank you for this solution but it possible to apply this method in an older version but use objects within tuple?

@ijunaid8989
Copy link

I have multiple such objects in array. But I am on an older version of Yup and not sure how to apply this

[
    {
        "type": "object",
        "_whitelist": {
            "list": {},
            "refs": {}
        },
        "_blacklist": {
            "list": {},
            "refs": {}
        },
        "exclusiveTests": {},
        "deps": [],
        "conditions": [],
        "tests": [],
        "transforms": [
            null
        ],
        "spec": {
            "strip": false,
            "strict": false,
            "abortEarly": true,
            "recursive": true,
            "nullable": false,
            "presence": "optional"
        },
        "fields": {
            "name": {
                "deps": [],
                "tests": [],
                "transforms": [
                    null
                ],
                "conditions": [],
                "_whitelist": {
                    "list": {},
                    "refs": {}
                },
                "_blacklist": {
                    "list": {},
                    "refs": {}
                },
                "exclusiveTests": {},
                "spec": {
                    "strip": false,
                    "strict": false,
                    "abortEarly": true,
                    "recursive": true,
                    "nullable": false,
                    "presence": "optional"
                },
                "type": "string"
            },
            "email": {
                "type": "string",
                "_whitelist": {
                    "list": {},
                    "refs": {}
                },
                "_blacklist": {
                    "list": {},
                    "refs": {}
                },
                "exclusiveTests": {
                    "email": false,
                    "required": true
                },
                "deps": [],
                "conditions": [],
                "tests": [
                    null,
                    null
                ],
                "transforms": [
                    null
                ],
                "spec": {
                    "strip": false,
                    "strict": false,
                    "abortEarly": true,
                    "recursive": true,
                    "nullable": false,
                    "presence": "required"
                }
            },
            "phoneNumber": {
                "deps": [],
                "tests": [],
                "transforms": [
                    null
                ],
                "conditions": [],
                "_whitelist": {
                    "list": {},
                    "refs": {}
                },
                "_blacklist": {
                    "list": {},
                    "refs": {}
                },
                "exclusiveTests": {},
                "spec": {
                    "strip": false,
                    "strict": false,
                    "abortEarly": true,
                    "recursive": true,
                    "nullable": false,
                    "presence": "optional"
                },
                "type": "string"
            },
            "extension": {
                "deps": [],
                "tests": [],
                "transforms": [
                    null
                ],
                "conditions": [],
                "_whitelist": {
                    "list": {},
                    "refs": {}
                },
                "_blacklist": {
                    "list": {},
                    "refs": {}
                },
                "exclusiveTests": {},
                "spec": {
                    "strip": false,
                    "strict": false,
                    "abortEarly": true,
                    "recursive": true,
                    "nullable": false,
                    "presence": "optional"
                },
                "type": "string"
            }
        },
        "_nodes": [
            "extension",
            "phoneNumber",
            "email",
            "name"
        ],
        "_excludedEdges": []
    }
]

@samkit5495
Copy link

Not sure if it'd work but, did you try treating the array as an object? Something like:

const schema = object().shape({
  0: string(), 
  1: number(),
  2: boolean(),
  length: number()
})

This doesn't work
https://runkit.com/samkit5495/63a6b77367b81e00084ce74c

@yurisldk
Copy link

yurisldk commented May 4, 2023

I had luck adding a new method to Yup.array() ~> Yup.array().tuple(). You can then define a schema with a tuple and validate a tuple - the new method takes care of the transformation.

Yup.addMethod(Yup.array, 'tuple', function(schema) {
  if (!this.isType(schema)) Yup.ValidationError();
  return Yup.object({
    tuple: Yup.array().min(schema.length).max(schema.length),
    ...Object.fromEntries(Object.entries(schema)),
  }).transform((value, originalValue) => {
    if (!this.isType(originalValue)) Yup.ValidationError();
    return {
      tuple: originalValue,
      ...Object.fromEntries(Object.entries(originalValue))
    };
  });
});

// Create your schema
const MySchema = Yup.array().tuple([
  Yup.number(),
  Yup.string(),
  Yup.boolean(),
]);

// Validate your tuple
const { tuple } = await MySchema.validate([1999, "ja", true]);

console.log(tuple); // [1999, "ja", true]

Check it out on RunKit - [here]

you also may to use the build-in method tuple for fixed length arrays.

import { tuple, string, number, InferType } from 'yup';

let schema = tuple([
  string().label('name'),
  number().label('age').positive().integer(),
]);

await schema.validate(['James', 3]); // ['James', 3]

await schema.validate(['James', -24]); // => ValidationError: age must be a positive number

InferType<typeof schema> // [string, number] | undefined

@huynhphucdienncc
Copy link

huynhphucdienncc commented Jun 27, 2023

You can use when, then to validate any item in your array:

 campaigns: yup
            .array(
                yup.object().shape({
                    config: yup.object().when(['isActive', 'type'], {
                        is: (isActive: boolean, type: CreateEventCampaignsDtoTypeEnum) => {
                            return isActive && type === CreateEventCampaignsDtoTypeEnum.EmailPromotion;
                        },
                        then: yup.object().shape({
                            title: yup
                                .string()
                                .max(45, t('error.maxCampaignTitle'))
                                .required(t('error.campaignTitle'))
                                .test('blankValid', t('error.campaignTitle'), (value: string) => value?.trim()?.length > 0),
                            message: yup
                                .string()
                                .max(800, t('error.maxCampaignMessage'))
                                .required(t('error.campaignMessage'))
                                .test('blankValid', t('error.campaignMessage'), (value: string) => value?.trim()?.length > 0),
                            imageFile: yup.object().required(t('error.campaignImageFile')),
                            link: yup
                                .string()
                                .required(t('error.campaignLink'))
                                .test('blankValid', t('error.campaignLink'), (value: string) => value?.trim()?.length > 0)
                                .test('validLink', t('error.campaignLinkInvalid'), (value: string) => value?.trim()?.startsWith('http')),
                        }),
                    }),
                }),
            )
            .nullable(),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests