Yup: How to conditionally validate at least one of n values are set?

Created on 20 Feb 2018  路  21Comments  路  Source: jquense/yup

I have 5 form fields, at last one of which must be populated for validation to succeed. I attempted this, but soon ran into cyclic reference issues:

tag_business: yup.string().when(['tag_cost_centre', 'tag_project_code', 'tag_internal_order', 'tag_business'], {
  is: (tag_cost_centre, tag_project_code, tag_internal_order, tag_business) => {
    return !(tag_cost_centre, tag_project_code, tag_internal_order, tag_business)
  },
  then: yup.string().required()
})

What would be the best way to implement this?

Most helpful comment

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c'], {
    is: (b, c) => !b && !c,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c'], {
    is: (a, c) => !a && !c,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b'], {
    is: (a, b) => !a && !b,
    then: yup.string().required()
  })
}, [['a', 'b'], ['a', 'c'], ['b','c']]) // <-- HERE!!!!!!!!

All 21 comments

That's right but don't include the field itself there. That's why you are getting cycles, your telling yup the field depends on itself, which doesn't make sense

That makes sense, however, I have fixed the issue and am still getting cyclic errors. Here's a reduced test case:

https://runkit.com/nandastone/5a8ceae6d80f770012eb7270

Is yup running validation for sibling fields when they are referenced in when()?

Yeah this is a bit of a gotcha. Fields that depend on each other need to sorted so they are "constructed" in the right order, e.g. if depend on field A in field B, yup needs to cast and coerce the value in field A before it's handed to B. What's happening here tho is you are only adding a validation in the condition, so there isn't _actually_ any need to order anything validation happens after everything is constructed already. because of the flexibility and programmatic nature of yup it can't distinguish between those two cases.

There is an ugly escape hatch: using the shape() method directly that tells yup to ignore the ordering for a pair of edges: https://runkit.com/5908cecfe97ebf0012f2e3c9/5a8d87d03a470f00122666d8

I don't quite understand the low-level reason you've explained, but appreciate you taking the time to help.

Your example worked well, until I extended the validation to depend on more than one other field:

https://runkit.com/nandastone/5a8e0bf15ae96a0012e235a7

Thoughts?

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c'], {
    is: (b, c) => !b && !c,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c'], {
    is: (a, c) => !a && !c,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b'], {
    is: (a, b) => !a && !b,
    then: yup.string().required()
  })
}, [['a', 'b'], ['a', 'c'], ['b','c']]) // <-- HERE!!!!!!!!

Has there been a resolution to this? I am experiencing the same thing. I upgraded the code a bit in one of the links to this and get cyclic dependency issue

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c', 'd'], {
    is: (b, c, d) => !b && !c && !d,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c', 'd'], {
    is: (a, c, d) => !a && !c && !d,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b', 'd'], {
    is: (a, b, d) => !a && !b && !d,
    then: yup.string().required()
  }),
  d: yup.string().when(['a', 'b', 'c'], {
    is: (a, b, c) => !a && !b && !c,
    then: yup.string().required()
  })
}, [['a', 'b', 'c'], ['b', 'c', 'd'], ['a','c', 'd'], ['a','b','d']]) // <-- HERE!!!!!!!!

Hi @pmonty the issue in your last snippet I believe is because you aren't properly enumerating the list of 'edges' in your validation schema.

Pretty much in the array of combinations at the end of the .shape(...) call you need to list all the pair-wise (2-tuples) combinations - in your example you have triples (i.e. ['a','b','c']).
Because you have 4 fields, you'd expect to have 6 pairs in your array.

Anyway, I did this and it worked fine for me.

I have an array of objects, and for each object, I have dependent fields. My code:

const schema = Yup.object().shape({
  colleagues: Yup.array().of(
    Yup.object().shape({
      name: Yup.string().when('role', {
        is: role => role.length > 0,
        then: Yup.string().required('Colleague name is required.')
      }),
      role: Yup.string().when('name', {
        is: name => name.length > 0,
        then: Yup.string().required('Colleague role is required.')
      })
    }, ['name', 'role'])
  ),
  email: Yup.string()
    .email('Email is not valid.')
    .required('Email is required.'),
});

When I remove the when validation on name or role, the validation works perfectly, and I receive errors for all fields. However, when I include both, and enumerate the dependencies of the array objects, I don't receive errors for anything, including the top-level email field. Since these are objects inside of an array, am I specifying the paths incorrectly?

The solutions here seem straightforward enough, so I'm sure I'm missing something small. Any help would be much appreciated.

It turns out that role and name were coming back as undefined, so the validation never got to the conditional check of the object in the array. Changing the conditional check to role && role.length > 0 seems to have resolved this issue.

@jmpolitzer Thanks, it saved me!

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c'], {
    is: (b, c) => !b && !c,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c'], {
    is: (a, c) => !a && !c,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b'], {
    is: (a, b) => !a && !b,
    then: yup.string().required()
  })
}, [['a', 'b'], ['a', 'c'], ['b','c']]) // <-- HERE!!!!!!!!

This does not work anymore! Turns out that is function now only accepts 1 param. So now I can validate at most 2 fields of which 1 is required.

So the type of is function is now boolean | ((value: any) => boolean). Any reason why this has changed? @jquense

For now I got it work by changing the type to boolean | ((...value: any) => boolean). Any chance this will be fixed in a coming release?

@loolooii, I ran into the same TypeScript issue, and it looks like you just have to upgrade @types/yup to v0.26.14 or later. The issue was fixed in https://github.com/DefinitelyTyped/DefinitelyTyped/pull/35885.

instead of using examples above, i just created a 'ghost' field in yup validation schema that checks specified fields, alot less code and no cyclic errors

yup.object().shape({
        a: yup.string(),
        b: yup.string(),
        AorB: yup.bool().when(['a', 'b'], {
            is: (a, b) => (!a && !b) || (!!a  && !!b),
            then: yup.bool().required('some error msg'),
            otherwise: yup.bool()
        })
    })

This will generate an error and prevent submit.

When using Formik Fields just remember to include additional errors from this 'ghost' field, like:

<Field
        error={!!(errors && errors.a || (errors as any).AorB)}
        helperText={errors && errors.b || (errors as any).AorB)}
/>

(errors as any) because TypeScript will not see this error (field is not in initialValues)

I only want to check heightUnit if heightValue is > 0.

```
heightValue: Yup.number().positive().when(['heightUnit'], {
is: val => !!val,
then: Yup.number(),
otherwise: Yup.number().negative('Unit is required')
})

https://github.com/jquense/yup/issues/176#issuecomment-442977916 This comment save my day !

#176 (comment) This comment save my day !

Could you please provide me an example? I couldn't get it working :(

This works for me:

const phoneRegex = /^\+([0-9]{1,3})*\.([0-9]{5,16})$/;

yup.object().shape({
    phoneNumber: yup.string()
        .matches(phoneRegex)
        .when('mobileNumber', {
            is: val => !!val,
            then: yup.string(),
            otherwise: yup.string()required('phone or mobile number required')
        }),
    mobileNumber: yup.string()
        .matches(phoneRegex)
        .when('phoneNumber', {
            is: val => !!val,
            then: yup.string(),
            otherwise: yup.string().required('phone or mobile number required')
        })
}, [['mobileNumber', 'phoneNumber']]);

Hi @pmonty the issue in your last snippet I believe is because you aren't properly enumerating the list of 'edges' in your validation schema.

Pretty much in the array of combinations at the end of the .shape(...) call you need to list all the pair-wise (2-tuples) combinations - in your example you have triples (i.e. ['a','b','c']).
Because you have 4 fields, you'd expect to have 6 pairs in your array.

Anyway, I did this and it worked fine for me.

this is working for me

const AddusersSchema= object().shape({ lastName: string().when(['firstname', 'emailId', 'userType1'], { is: (firstname,emailId,userType1 ) => firstname || emailid || userType1, then: string().required() }), firstname: string().when(['lastName', 'userType1','emailId'], { is: ( lastsame, emailId , userType1) => lastsame || emailId || userType1, then: string().required() }), emailId: string().when(['firstname', 'lastName', 'userType1'], { is: (firstname,lastsame1, userType1) => firstname || lastsame1 || userType1, then: string().required() }).matches(/(?=.*[@$!%*#?&])/, 'Please enter valid email.'), userType1: string().when(['firstname','lastname','emailId'], { is: ( lastsame, emailId , userType1) => lastsame || emailId || userType1, then: string().required() }) },[['firstname','lastName'], ['firstname', 'emailId'] , [ 'userType1','emailId'] , ['lastName', 'userType1'], ['lastName', 'emailId'], ['firstname','userType1']]);

var schema = yup.object().shape({
  a: yup.string().when(['b', 'c'], {
    is: (b, c) => !b && !c,
    then: yup.string().required()
  }),
  b: yup.string().when(['a', 'c'], {
    is: (a, c) => !a && !c,
    then: yup.string().required()
  }),
  c: yup.string().when(['a', 'b'], {
    is: (a, b) => !a && !b,
    then: yup.string().required()
  })
}, [['a', 'b'], ['a', 'c'], ['b','c']]) // <-- HERE!!!!!!!!

it is throwing Uncaught Error: Cyclic dependency, the node was: "b". Someone, Please, help me with the working code. I required only one of the fields

Is this still a problem or is there a documented solution?

Was this page helpful?
0 / 5 - 0 ratings