DEV Community

Keegan Good
Keegan Good

Posted on

Yup - context values in custom test error messages

Yup is a super popular object validation library written in TypeScript.

This post will assume a basic working knowledge of Yup.

Not only does Yup provide a large array of built-in validation functions, but it also allows users to customize tests to suit their needs. These custom test functions accept an options object, which allows for external values to be passed in to be used in writing the boolean logic within the test.

On the documentation page, they give this example:

const jamesSchema = string().test({
    name: 'is-james',
    message: (d) => `${d.path} is not James`,
    test: (value) => value == null || value === 'James',
  }
);

jamesSchema.validateSync('James'); // "James"

jamesSchema.validateSync('Jane'); // ValidationError "this is not James"
Enter fullscreen mode Exit fullscreen mode

But what if the name to be tested is not "James"? What if we want to pass in a name against which to validate? Well, the validate and validateAt functions, as well as their sync alternatives accept an options object, which can include external values. The options object is included as part of the context object passed to the body of the test function. More info here.

If we adjust the previous example, we can pull the name out of the context options object within the test function.

const nameSchema= string().test({
    name: 'is-name',
    message: (d) => `${d.path} is not James`,
    test: (value, context) => (
      value !== null &&
      value === context.options.targetName
    ),
  }
);
Enter fullscreen mode Exit fullscreen mode

This will throw a ValidationError with the message this is not James. However, the name "James" is hardcoded into the error message and has no relation to the target value stored in context.options.targetName.

If we log the value of context within the test function, it looks like this:

{
  "path": "",
  "type": "is-name",
  "options": {
    "targetName": "Ringo",
    "sync": true
  },
  "originalValue": "James",
  "schema": {
    // ...excluded for brevity
  }
} 
Enter fullscreen mode Exit fullscreen mode

As we can see, the targetName property is included in the options object within the context object.

However, if we log the object that's passed to the message function, we see something different.

{
  "value": "James",
  "originalValue": "James",
  "path": "this",
  "spec": {
    "strip": false,
    "strict": false,
    "abortEarly": true,
    "recursive": true,
    "nullable": false,
    "optional": true,
    "coerce": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Uh-oh, the options object isn't included in the object passed to the message function. So, what do we do if we need to pass a value through the context object to be used in the error message?

Well, the solution is to forego use of the message function entirely. The context object in the test function has a createError function which can be used to format a Yup ValidationError message. The result of this function can be returned if the resulting boolean from the test function is false and because it is in the scope of the test function, it still has access to the context values passed to the function.

const nameSchema = string().test({
    name: 'is-name',
    test: (value, context) => (
      value !== null &&
      value === context.options.targetName
    ) || context.createError(`${context.path} is not ${context.options.targetName}`),
  }
);
Enter fullscreen mode Exit fullscreen mode

Now the target name value will be formatted into the resulting ValidationError message.

Here's a working example.

It would be great if the context object would be passed to the message function as well. It would simplify the error-handling logic and isolate it from the test logic. There is an open issue on Yup's Github page about this, so hopefully it'll get resolved in an upcoming update release.

Top comments (0)