Yup - context values in custom test error messages
Published: 2023-06-12 17:10:21

[Yup](https://github.com/jquense/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:

```javascript
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"
```

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](https://github.com/jquense/yup#schemavalidatevalue-any-options-object-promiseinfertypeschema-validationerror).

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

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

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:

```javascript
{
  "path": "",
  "type": "is-name",
  "options": {
    "targetName": "Ringo",
    "sync": true
  },
  "originalValue": "James",
  "schema": {
    // ...excluded for brevity
  }
} 
```

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.

```javascript
{
  "value": "James",
  "originalValue": "James",
  "path": "this",
  "spec": {
    "strip": false,
    "strict": false,
    "abortEarly": true,
    "recursive": true,
    "nullable": false,
    "optional": true,
    "coerce": true
  }
}
```

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.

```javascript
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}`),
  }
);
```

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

Here's a working [example](https://codesandbox.io/s/yup-context-values-in-custom-test-error-message-gz95kh?file=/src/index.js).

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](https://github.com/jquense/yup/issues/1995) on Yup's Github page about this, so hopefully it'll get resolved in an upcoming update release.