jquense / yup

Dead simple Object schema validation
MIT License
22.89k stars 933 forks source link

Referencing a field outside the current yup object in a `when` function #2173

Open kavlih opened 9 months ago

kavlih commented 9 months ago

I'm trying to reference a field outside the current yup object. In this example, car should only be required when name==='Pete'. I found that the when function in Yup only has access to the current level of the schema. And because the name field is outside the additionals object, referencing name in the when function wont work.

  const schema = yup.object({
    additionals: yup.object({
      car: yup
        .string()
        .when(["name"], ([name], schema) =>
          name === "Pete"
            ? schema.required("required")
            : schema.optional().nullable()
        ),
    }),
    name: yup.string().optional().nullable(),
  });

I read that "The $ symbol is used by Yup to reference fields from the root level of the schema." so i tried the same schema but using the $, which didn't work

        .when(["$name"], ([name], schema) =>

I got it solved with putting the when function on the object level and using two seperate schemas for both cases:

    additionals: Yup.object().when('name ', ([name ], schema) => {
        return name === 'lapid'
            ? schema.shape({
                car: Yup.string().required('required'),
            })
            : schema.shape({
                car: Yup.string().optional().nullable(),
            });
    }),
    name: yup.string().optional().nullable(),

But this solution is not very pretty and brings other problems with it. If i have other fields in the additionals object, i have to declare them in both schemas, even tho they might not be a part of the condition and i get a duplicated code. Also if i want to step through my form object (the type is based on the Yup schema) like additionals.car i get the TypeScript Error TS2339: Property car does not exist on type  {}. So this solution wont really work for me.

I tried to make a test case with the provided example. Not quite sure how it works and if i set it up correctly, but any feedback is welcomed: Codesandbox

Any help is appreciated.

Hipnosis183 commented 9 months ago

You can use the $ symbol by passing a context object on the validate() function call.

Using your first example:

const schema = yup.object({
  additionals: yup.object({
    car: yup
      .string()
      .when(["$name"], ([name], schema) =>
        name === "Pete"
          ? schema.required("required")
          : schema.optional().nullable()
      ),
  }),
  name: yup.string().optional().nullable(),
});

schema.validate(myDataToValidate, { context: myDataToValidate });

If you have nested data you can use dot notation, e.g. $additionals.car.

ebrannin-bw commented 5 months ago

I don't want to change what Context is being sent for validation (Formik is handling that), so I'm working around this issue by adding a yup.test() at the end of the schema, like this:

const schema = yup.object({
  additionals: yup.object({
    car: yup.string().nullable()
  }),
  name: yup.string().optional().nullable(),
}).test({
  name: 'peteNeedsACar',
  message: 'A car is required for Pete',
  test: (value) => value.name !== 'Pete' || value.additionals.car.length !== 0,
});

Edit: This works in unit tests on the schema, but Formik seems to be ignoring it. I haven't yet figured out why.

Edit 2: My current code for this is throwing a ValidationError instead of returning true/false -- but I also had a null-vs-emptystring issue that was throwing exceptions in the UI, so I'm not sure if the exception was required.

I don't plan to check if going from ValidationError back to true/false would work, because the ValidationError is what's telling Formik which field has a problem.