zod-validation-error
Version:
Wrap zod validation errors in user-friendly readable messages
559 lines (398 loc) • 19.2 kB
Markdown
# zod-validation-error
Wrap zod validation errors in user-friendly readable messages.
[](https://github.com/causaly/zod-validation-error/actions/workflows/ci.yml) [](https://www.npmjs.com/package/zod-validation-error)
#### Features
- User-friendly readable messages, configurable via options;
- Maintain original issues under `error.details`;
- Supports both `zod` v3 and v4.
**_Note:_** This is the v3 version of `zod-validation-error`. If you are looking for zod v4 support, please click [here](/README.md).
## Installation
```bash
npm install zod-validation-error
```
#### Requirements
- Node.js v.18+
- TypeScript v.4.5+
## Quick start
```typescript
import { z as zod } from 'zod/v3';
import { fromError } from 'zod-validation-error/v3';
// create zod schema
const zodSchema = zod.object({
id: zod.number().int().positive(),
email: zod.string().email(),
});
// parse some invalid value
try {
zodSchema.parse({
id: 1,
email: 'foobar', // note: invalid email
});
} catch (err) {
const validationError = fromError(err);
// the error is now readable by the user
// you may print it to console
console.log(validationError.toString());
// or return it as an actual error
return validationError;
}
```
## Motivation
Zod errors are difficult to consume for the end-user. This library wraps Zod validation errors in user-friendly readable messages that can be exposed to the outer world, while maintaining the original errors in an array for _dev_ use.
### Example
#### Input (from Zod)
```json
[
{
"code": "too_small",
"inclusive": false,
"message": "Number must be greater than 0",
"minimum": 0,
"path": ["id"],
"type": "number"
},
{
"code": "invalid_string",
"message": "Invalid email",
"path": ["email"],
"validation": "email"
}
]
```
#### Output
```
Validation error: Number must be greater than 0 at "id"; Invalid email at "email"
```
## API
- [ValidationError(message[, options])](#validationerror)
- [createMessageBuilder(props)](#createMessageBuilder)
- [errorMap](#errormap)
- [isValidationError(error)](#isvalidationerror)
- [isValidationErrorLike(error)](#isvalidationerrorlike)
- [isZodErrorLike(error)](#iszoderrorlike)
- [fromError(error[, options])](#fromerror)
- [fromZodIssue(zodIssue[, options])](#fromzodissue)
- [fromZodError(zodError[, options])](#fromzoderror)
- [toValidationError([options]) => (error) => ValidationError](#tovalidationerror)
### ValidationError
Main `ValidationError` class, extending native JavaScript `Error`.
#### Arguments
- `message` - _string_; error message (required)
- `options` - _ErrorOptions_; error options as per [JavaScript definition](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error#options) (optional)
- `options.cause` - _any_; can be used to hold the original zod error (optional)
#### Example 1: construct new ValidationError with `message`
```typescript
const { ValidationError } = require('zod-validation-error');
const error = new ValidationError('foobar');
console.log(error instanceof Error); // prints true
```
#### Example 2: construct new ValidationError with `message` and `options.cause`
```typescript
import { z as zod } from 'zod/v3';
const { ValidationError } = require('zod-validation-error');
const error = new ValidationError('foobar', {
cause: new zod.ZodError([
{
code: 'invalid_string',
message: 'Invalid email',
path: ['email'],
validation: 'email',
},
]),
});
console.log(error.details); // prints issues from zod error
```
### createMessageBuilder
Creates zod-validation-error's default `MessageBuilder`, which is used to produce user-friendly error messages.
Meant to be passed as an option to [fromError](#fromerror), [fromZodIssue](#fromzodissue), [fromZodError](#fromzoderror) or [toValidationError](#tovalidationerror).
You may read more on the concept of the `MessageBuilder` further [below](#MessageBuilder).
#### Arguments
- `props` - _Object_; formatting options (optional)
- `maxIssuesInMessage` - _number_; max issues to include in user-friendly message (optional, defaults to 99)
- `issueSeparator` - _string_; used to concatenate issues in user-friendly message (optional, defaults to ";")
- `unionSeparator` - _string_; used to concatenate union-issues in user-friendly message (optional, defaults to ", or")
- `prefix` - _string_ or _null_; prefix to use in user-friendly message (optional, defaults to "Validation error"). Pass `null` to disable prefix completely.
- `prefixSeparator` - _string_; used to concatenate prefix with rest of the user-friendly message (optional, defaults to ": "). Not used when `prefix` is `null`.
- `includePath` - _boolean_; used to provide control on whether to include the erroneous property name suffix or not (optional, defaults to `true`).
#### Example
```typescript
import { createMessageBuilder } from 'zod-validation-error/v3';
const messageBuilder = createMessageBuilder({
includePath: false,
maxIssuesInMessage: 3,
});
```
### errorMap
A custom error map to use with zod's `setErrorMap` method and get user-friendly messages automatically.
#### Example
```typescript
import { z as zod } from 'zod/v3';
import { errorMap } from 'zod-validation-error/v3';
zod.setErrorMap(errorMap);
```
### isValidationError
A [type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) utility function, based on `instanceof` comparison.
#### Arguments
- `error` - error instance (required)
#### Example
```typescript
import { z as zod } from 'zod/v3';
import { ValidationError, isValidationError } from 'zod-validation-error/v3';
const err = new ValidationError('foobar');
isValidationError(err); // returns true
const invalidErr = new Error('foobar');
isValidationError(err); // returns false
```
### isValidationErrorLike
A [type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) utility function, based on _heuristics_ comparison.
_Why do we need heuristics since we can use a simple `instanceof` comparison?_ Because of multi-version inconsistencies. For instance, it's possible that a dependency is using an older `zod-validation-error` version internally. In such case, the `instanceof` comparison will yield invalid results because module deduplication does not apply at npm/yarn level and the prototype is different.
tl;dr if you are uncertain then it is preferable to use `isValidationErrorLike` instead of `isValidationError`.
#### Arguments
- `error` - error instance (required)
#### Example
```typescript
import {
ValidationError,
isValidationErrorLike,
} from 'zod-validation-error/v3';
const err = new ValidationError('foobar');
isValidationErrorLike(err); // returns true
const invalidErr = new Error('foobar');
isValidationErrorLike(err); // returns false
```
### isZodErrorLike
A [type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) utility function, based on _heuristics_ comparison.
_Why do we need heuristics since we can use a simple `instanceof` comparison?_ Because of multi-version inconsistencies. For instance, it's possible that a dependency is using an older `zod` version internally. In such case, the `instanceof` comparison will yield invalid results because module deduplication does not apply at npm/yarn level and the prototype is different.
#### Arguments
- `error` - error instance (required)
#### Example
```typescript
import { z as zod } from 'zod/v3';
import { ValidationError, isZodErrorLike } from 'zod-validation-error/v3';
const zodValidationErr = new ValidationError('foobar');
isZodErrorLike(zodValidationErr); // returns false
const genericErr = new Error('foobar');
isZodErrorLike(genericErr); // returns false
const zodErr = new zod.ZodError([
{
code: zod.ZodIssueCode.custom,
path: [],
message: 'foobar',
fatal: true,
},
]);
isZodErrorLike(zodErr); // returns true
```
### fromError
Converts an error to `ValidationError`.
_What is the difference between `fromError` and `fromZodError`?_ The `fromError` function is a less strict version of `fromZodError`. It can accept an unknown error and attempt to convert it to a `ValidationError`.
#### Arguments
- `error` - _unknown_; an error (required)
- `options` - _Object_; formatting options (optional)
- `messageBuilder` - _MessageBuilder_; a function that accepts an array of `zod.ZodIssue` objects and returns a user-friendly error message in the form of a `string` (optional).
#### Notes
Alternatively, you may pass the following `options` instead of a `messageBuilder`.
- `options` - _Object_; formatting options (optional)
- `maxIssuesInMessage` - _number_; max issues to include in user-friendly message (optional, defaults to 99)
- `issueSeparator` - _string_; used to concatenate issues in user-friendly message (optional, defaults to ";")
- `unionSeparator` - _string_; used to concatenate union-issues in user-friendly message (optional, defaults to ", or")
- `prefix` - _string_ or _null_; prefix to use in user-friendly message (optional, defaults to "Validation error"). Pass `null` to disable prefix completely.
- `prefixSeparator` - _string_; used to concatenate prefix with rest of the user-friendly message (optional, defaults to ": "). Not used when `prefix` is `null`.
- `includePath` - _boolean_; used to provide control on whether to include the erroneous property name suffix or not (optional, defaults to `true`).
They will be passed as arguments to the [createMessageBuilder](#createMessageBuilder) function. The only reason they exist is to provide backwards-compatibility with older versions of `zod-validation-error`. They should however be considered deprecated and may be removed in the future.
### fromZodIssue
Converts a single zod issue to `ValidationError`.
#### Arguments
- `zodIssue` - _zod.ZodIssue_; a ZodIssue instance (required)
- `options` - _Object_; formatting options (optional)
- `messageBuilder` - _MessageBuilder_; a function that accepts an array of `zod.ZodIssue` objects and returns a user-friendly error message in the form of a `string` (optional).
#### Notes
Alternatively, you may pass the following `options` instead of a `messageBuilder`.
- `options` - _Object_; formatting options (optional)
- `issueSeparator` - _string_; used to concatenate issues in user-friendly message (optional, defaults to ";")
- `unionSeparator` - _string_; used to concatenate union-issues in user-friendly message (optional, defaults to ", or")
- `prefix` - _string_ or _null_; prefix to use in user-friendly message (optional, defaults to "Validation error"). Pass `null` to disable prefix completely.
- `prefixSeparator` - _string_; used to concatenate prefix with rest of the user-friendly message (optional, defaults to ": "). Not used when `prefix` is `null`.
- `includePath` - _boolean_; used to provide control on whether to include the erroneous property name suffix or not (optional, defaults to `true`).
They will be passed as arguments to the [createMessageBuilder](#createMessageBuilder) function. The only reason they exist is to provide backwards-compatibility with older versions of `zod-validation-error`. They should however be considered deprecated and may be removed in the future.
### fromZodError
Converts zod error to `ValidationError`.
_Why is the difference between `ZodError` and `ZodIssue`?_ A `ZodError` is a collection of 1 or more `ZodIssue` instances. It's what you get when you call `zodSchema.parse()`.
#### Arguments
- `zodError` - _zod.ZodError_; a ZodError instance (required)
- `options` - _Object_; formatting options (optional)
- `messageBuilder` - _MessageBuilder_; a function that accepts an array of `zod.ZodIssue` objects and returns a user-friendly error message in the form of a `string` (optional).
#### Notes
Alternatively, you may pass the following `options` instead of a `messageBuilder`.
- `options` - _Object_; formatting options (optional)
- `maxIssuesInMessage` - _number_; max issues to include in user-friendly message (optional, defaults to 99)
- `issueSeparator` - _string_; used to concatenate issues in user-friendly message (optional, defaults to ";")
- `unionSeparator` - _string_; used to concatenate union-issues in user-friendly message (optional, defaults to ", or")
- `prefix` - _string_ or _null_; prefix to use in user-friendly message (optional, defaults to "Validation error"). Pass `null` to disable prefix completely.
- `prefixSeparator` - _string_; used to concatenate prefix with rest of the user-friendly message (optional, defaults to ": "). Not used when `prefix` is `null`.
- `includePath` - _boolean_; used to provide control on whether to include the erroneous property name suffix or not (optional, defaults to `true`).
They will be passed as arguments to the [createMessageBuilder](#createMessageBuilder) function. The only reason they exist is to provide backwards-compatibility with older versions of `zod-validation-error`. They should however be considered deprecated and may be removed in the future.
### toValidationError
A curried version of `fromZodError` meant to be used for FP (Functional Programming). Note it first takes the options object if needed and returns a function that converts the `zodError` to a `ValidationError` object
```js
toValidationError(options) => (zodError) => ValidationError
```
#### Example using fp-ts
```typescript
import * as Either from 'fp-ts/Either';
import { z as zod } from 'zod/v3';
import { toValidationError, ValidationError } from 'zod-validation-error/v3';
// create zod schema
const zodSchema = zod
.object({
id: zod.number().int().positive(),
email: zod.string().email(),
})
.brand<'User'>();
export type User = zod.infer<typeof zodSchema>;
export function parse(
value: zod.input<typeof zodSchema>
): Either.Either<ValidationError, User> {
return Either.tryCatch(() => schema.parse(value), toValidationError());
}
```
## MessageBuilder
`zod-validation-error` can be configured with a custom `MessageBuilder` function in order to produce case-specific error messages.
#### Example
For instance, one may want to print `invalid_string` errors to the console in red color.
```typescript
import { z as zod } from 'zod/v3';
import { type MessageBuilder, fromError } from 'zod-validation-error/v3';
import chalk from 'chalk';
// create custom MessageBuilder
const myMessageBuilder: MessageBuilder = (issues) => {
return (
issues
// format error message
.map((issue) => {
if (issue.code === zod.ZodIssueCode.invalid_string) {
return chalk.red(issue.message);
}
return issue.message;
})
// join as string with new-line character
.join('\n')
);
};
// create zod schema
const zodSchema = zod.object({
id: zod.number().int().positive(),
email: zod.string().email(),
});
// parse some invalid value
try {
zodSchema.parse({
id: 1,
email: 'foobar', // note: invalid email value
});
} catch (err) {
const validationError = fromError(err, {
messageBuilder: myMessageBuilder,
});
// the error is now displayed with red letters
console.log(validationError.toString());
}
```
## FAQ
### How to distinguish between errors
Use the `isValidationErrorLike` type guard.
#### Example
Scenario: Distinguish between `ValidationError` VS generic `Error` in order to respond with 400 VS 500 HTTP status code respectively.
```typescript
import * as Either from 'fp-ts/Either';
import { z as zod } from 'zod/v3';
import { isValidationErrorLike } from 'zod-validation-error/v3';
try {
func(); // throws Error - or - ValidationError
} catch (err) {
if (isValidationErrorLike(err)) {
return 400; // Bad Data (this is a client error)
}
return 500; // Server Error
}
```
### How to use `ValidationError` outside `zod`
It's possible to implement custom validation logic outside `zod` and throw a `ValidationError`.
#### Example 1: passing custom message
```typescript
import { ValidationError } from 'zod-validation-error/v3';
import { Buffer } from 'node:buffer';
function parseBuffer(buf: unknown): Buffer {
if (!Buffer.isBuffer(buf)) {
throw new ValidationError('Invalid argument; expected buffer');
}
return buf;
}
```
#### Example 2: passing custom message and original error as cause
```typescript
import { ValidationError } from 'zod-validation-error/v3';
try {
// do something that throws an error
} catch (err) {
throw new ValidationError('Something went deeply wrong', { cause: err });
}
```
### How to use `ValidationError` with custom "error map"
Zod supports customizing error messages by providing a custom "error map". You may combine this with `zod-validation-error` to produce user-friendly messages.
#### Example 1: produce user-friendly error messages using the `errorMap` property
If all you need is to produce user-friendly error messages you may use the `errorMap` property.
```typescript
import { z as zod } from 'zod/v3';
import { errorMap } from 'zod-validation-error/v3';
zod.setErrorMap(errorMap);
```
#### Example 2: extra customization using `fromZodIssue`
If you need to customize some error code, you may use the `fromZodIssue` function.
```typescript
import { z as zod } from 'zod/v3';
import { fromZodIssue } from 'zod-validation-error/v3';
const customErrorMap: zod.ZodErrorMap = (issue, ctx) => {
switch (issue.code) {
case ZodIssueCode.invalid_type: {
return {
message:
'Custom error message of your preference for invalid_type errors',
};
}
default: {
const validationError = fromZodIssue({
...issue,
// fallback to the default error message
// when issue does not have a message
message: issue.message ?? ctx.defaultError,
});
return {
message: validationError.message,
};
}
}
};
zod.setErrorMap(customErrorMap);
```
### How to use `zod-validation-error` with `react-hook-form`?
```typescript
import { useForm } from 'react-hook-form';
import { zodResolver } from '/resolvers/zod';
import { errorMap } from 'zod-validation-error/v3';
useForm({
resolver: zodResolver(schema, { errorMap }),
});
```
### Does `zod-validation-error` support CommonJS
Yes, `zod-validation-error` supports CommonJS out-of-the-box. All you need to do is import it using `require`.
#### Example
```typescript
const { ValidationError } = require('zod-validation-error');
```
## Contribute
Source code contributions are most welcome. Please open a PR, ensure the linter is satisfied and all tests pass.
#### We are hiring
Causaly is building the world's largest biomedical knowledge platform, using technologies such as TypeScript, React and Node.js. Find out more about our openings at https://jobs.ashbyhq.com/causaly.
## License
MIT