yup: Nested object validation with when not working properly.

When i’m trying to validate a nested object. within when, it is not validating the request properly.

const updateRecord = Yup.object().shape({
	title: Yup.string()
		.required()
		.label("Post title"),
	description: Yup.string()
		.required()
		.label("Post description"),
	is_update: Yup.boolean()
		.default(false)
		.required(),
	images: Yup.object().shape({
		logoImage: Yup.mixed().when("is_update", {
			is: true,
			then: Yup.mixed()
				.nullable()
				.test("fileFormat", "Only .png file allowed.", value => {
					if (!value) {
						return true;
					}
					return value && ["image/png"].includes(value.type);
				})
				.label("Logo image"),
			otherwise: Yup.mixed()
				.required()
				.test("fileFormat", "Only .png file allowed.", value => {
					if (!value) {
						return true;
					}
					return value && ["image/png"].includes(value.type);
				})
				.label("Logo image")
		}),
		backgroundImage: Yup.mixed().when("is_update", {
			is: true,
			then: Yup.mixed()
				.nullable()
				.test("fileFormat", "Only .png file allowed.", value => {
					if (!value) {
						return true;
					}
					return value && ["image/png"].includes(value.type);
				})
				.label("Background image"),
			otherwise: Yup.mixed()
				.required()
				.test("fileFormat", "Only .png file allowed.", value => {
					if (!value) {
						return true;
					}
					return value && ["image/png"].includes(value.type);
				})
				.label("Background image")
		})
	})
});

Here i’m validating form based on is_update key which can be either true or false. but I’m getting it undefined. i have tried to console using this.

is: value => {
	console.log(value);
	return true;
}

It is logging is_update as undefined.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 33
  • Comments: 17 (4 by maintainers)

Most upvoted comments

Those who encountered this issue, check from.

yup.string()
  .test('name', 'errorMessage', function(value) {
    const { from } = this;
    // 'from' will contain the ancestor tree. You can access like from[1], from[2].
})

A workaround I found is to use Yup.mixed.test()

Example

Yup.object<any>().shape({
    name: Yup.string(),
    gender: Yup.boolean(),
    car: Yup.object<any>().shape({
        model: Yup.string(),
        horsePower: Yup.number(),
        color: Yup.mixed().test(
            'testName',
            'My error message',
            function (this: TestContext, fieldValue: any): boolean {
                const objectToValidate = this.options.context.data; // This is the whole object that comes to the validation
                /*
                    Looks like this
                    {
                        name: 'nameToValidate',
                        gender: true,
                        car: {
                            model: 'Ford',
                            horsePower: 225,
                            color: 'red',
                        }
                    }
                */

                // Now you can take the property you need
                // and continue by custom validating it and returning 'true'
                // when it passes or 'false' when it fails
            },
        ),
    }),
});

Also, good to note that instead of false you can return this.createError({ message: "YourCustomErrorMessage" }); if you have different failures in the test.

At a minimum could you please document context.from?

I’m having to do

    yup
      .mixed()
      .test(
        'conditionally-required',
        'Required',
        (value, context) => {
          const otherValueInNestedSchema = context.from[1].value.myOtherValue;
          return value || otherValueInNestedSchema !== 'someValue';
        }
      )

and I really wish I knew exactly why it worked.

At a minimum could you please document context.from?

I’m having to do

    yup
      .mixed()
      .test(
        'conditionally-required',
        'Required',
        (value, context) => {
          const otherValueInNestedSchema = context.from[1].value.myOtherValue;
          return value || otherValueInNestedSchema !== 'someValue';
        }
      )

and I really wish I knew exactly why it worked.

Well, this was the best solution i could find, but it has a problem with Typescript: Unsafe assignment of an any value. and Unsafe member access [1] on an any value..

Adding $ to field names gives no result. I need to access to the field in the root of my schema, is there a way to have it?

@akmjenkins I already have tried your solution. but the thing is that value is coming as undefined only if i’m doing it in nested object. if i’ll compare it in parent object, everything works fine.

… logoImage: Yup.mixed().when(“is_update”, { is: value => !!value, // instead of is: true


[Maintain reference to the parent schema?](https://github.com/jquense/yup/pull/556)

@Jule- Set the context to the value being validated and you can use $switchValue

(async () => {

  const valueBeingValidated = {
      switchValue: "two",
      nestedObject: { subPropA: "whatever" },
  }

  try {
    await schema.validate(
      valueBeingValidated,
      { context: valueBeingValidated } // <-- this line puts it in context and allows you to do $switchValue
    );
  } catch (err) {
    console.error(err);
    process.exit(1);
  }
  console.log("Object is valid!");
  process.exit(0);
})();

@akmjenkins thanks for the response! I think I can make a workaround with this indeed. But I’ll need to wrap my schema before giving it to Formik which works directly with yup schema for validation out of the box (so normally I do not call validate myself).

It feels a little wonky though to give artificially the context side by side of the same actual “context” being validated (the first argument) while you define conditional validation directly in your schema.

Hope to see a smoother alternative in the future! 🙂 Thanks again for your time and good work! 😉 👍

@akmjenkins thanks for the quick response! 🙂 Unfortunately my explanation was a little misleading… By “context” I wanted to mean the upper schema full data object that we are validating. Maybe I am wrong but I can’t access upper schema properties with $ notation, right?

Here is my minimum repro use case:

const yup = require("yup");

const schema = yup.object().shape({
  switchValue: yup
    .string()
    .oneOf(["one", "two", "three"])
    .required("switchValue is required"),
  nestedObject: yup.object().shape({
    subPropA: yup.string().required("This prop is always required."),
    subPropB: yup.string().when("$switchValue", {
      is: (switchValue) => {
        console.log("switchValue ", switchValue); // always prints `undefined` and not `two` as expected
        return switchValue === "two";
      },
      then: yup
        .string()
        .required("This prop is required when option `two` is selected!"),
      otherwise: yup.string().optional(),
    }),
  }),
});

(async () => {
  try {
    await schema.validate({
      switchValue: "two",
      nestedObject: { subPropA: "whatever" },
    });
  } catch (err) {
    console.error(err);
    process.exit(1);
  }
  console.log("Object is valid!");
  process.exit(0);
})();

What I would like to have is switchValue added somehow to the context or closure in order to achieve my validation. The solution I was suggesting in my previous comment was something like that (taking the .test() method approach):

...
    subPropB: yup.string().when("unused", {
      is: function(this: ValidateContext, unused: any) {
        console.log("switchValue ", this.context.data.switchValue); // prints `two` as expected
        return this.context.data.switchValue === "two";
      },
...

ValidateContext being a new interface/type or something already existing, I do not know what is available under the hood.

Is that possible?

Thanks!