Library / SDK
causaly/zod-validation-error avatar
causaly/zod-validation-error

zod-validation-error: turning Zod issues into messages a user can read

Wrap zod validation errors in user-friendly readable messages

1,021 stars17 forksTypeScriptMIT

At a glance

What is it?
zod-validation-error wraps Zod validation errors in readable messages while keeping the original issues available for developers, and it offers a custom error map so the wording can be tuned. It works with both Zod v3 and v4, though the current major line targets v4.
Who is it for?
zod-validation-error fits a TypeScript service that returns validation failures to end users and currently leaks raw Zod issues into API responses. It does not fit a codebase that only needs dev-time assertions, and it does not remove the need to think about what your users should be told, because the formatting options change the wording and the verbosity but not the policy.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Zod issues are written for developers, not for the person filling in the form

Zod is good at describing what went wrong. The README's motivation section is blunt about the problem: Zod errors are difficult to consume for the end-user. The example shows what that means in practice, with an issue object carrying an origin, a code, a minimum, a path and a message, and another carrying a format and a regular expression for an email field.

The information is rich and machine readable, and that is precisely the problem. An array of issue objects is not something to put in a response body for someone who mistyped their address, and a developer reading a log wants the opposite of a prose message. The library resolves that by producing two views of the same failure.

The outer view is a single readable line. The README's output example reads: Validation error: Number must be greater than 0 at "id"; Invalid email at "email". The inner view is the original issues, preserved in an array for dev use, which the feature list describes as original error details accessible via error.details.

The audience for this is a TypeScript backend that validates user input and has to report failure. The package is small, MIT licensed, and has no runtime dependencies of its own worth mentioning, since it works on the issues Zod already produced.

Two ways in: a Zod error map, or a wrapper at the catch site

The API surface offers two distinct mechanisms, and understanding which is which explains most of the configuration options.

The first is createErrorMap, which returns what the library calls an errorMap, used to format issues into user-friendly messages. The README is explicit that this is an error map like all others, so it can be used directly with Zod, and it points at Zod's own error customisation documentation. The reasoning is stated plainly: zod's native error map is not user-friendly enough, so the project provides its own implementation.

The second is fromError, which takes the caught error and returns a ValidationError. Alongside it sit fromZodError for a Zod error, fromZodIssue for a single issue, and toValidationError, which returns a function that turns an error into a ValidationError. There are also predicates, isValidationError, isValidationErrorLike and isZodErrorLike, which exist so that code catching an error can tell whether it is already one of these.

The quick start shows both used together, with zod.config setting customError to createErrorMap() so that every parse in the process produces the nicer messages, and fromError used in the catch block to convert the thrown error into the ValidationError you return. The README notes that configuring the error map is optional, since you may also use your own custom error map or Zod's native one.

So there is a real choice here. Global configuration changes every parse in the process, which is convenient and consistent. The wrapper leaves the rest of your application on Zod's defaults and converts only at the boundary where you decide to expose the failure. Which one you pick changes what a developer sees in a stack trace elsewhere in the code, and that is worth deciding deliberately rather than by accident.

npm install, then parse something invalid

Installation is a single npm command, and the README lists the requirements as Node.js v.18+ and TypeScript v.4.5+:

bash
npm install zod-validation-error

A first use configures the error map and defines a schema:

ts
import { z as zod } from 'zod';
import { fromError, createErrorMap } from 'zod-validation-error';

zod.config({
  customError: createErrorMap(),
});

const zodSchema = zod.object({
  id: zod.int().positive(),
  email: zod.email(),
});

Then the failure is caught and converted:

ts
try {
  zodSchema.parse({
    id: 1,
    email: 'coyote@acme',
  });
} catch (err) {
  const validationError = fromError(err);
  console.log(validationError.toString());
}

What you should see is the single line from the README's output example, with both problems named in one message rather than a stack of issue objects. The ValidationError class extends JavaScript's native Error, so it can be returned as an actual error, and it accepts the standard ErrorOptions, with a cause field that can hold the original Zod error.

The class is also usable on its own, for a message that did not come from Zod at all, and constructing one with a cause that contains a ZodError populated with issues gives you a ValidationError whose details property prints the issues from that Zod error.

The formatting options decide how much you tell the user

The error map takes an options object, and these options are where the library's behaviour is actually decided. The README documents them one by one, and the defaults are conservative in an interesting way.

displayInvalidFormatDetails is a boolean that defaults to false, and it controls whether invalid format details such as a regular expression pattern appear in the message. The example input in the README contains a full email regex in the issue, and leaving that out of the user-facing text is the default behaviour.

reportInput accepts false, type or typeAndValue, and defaults to type. It controls whether the user input is reported back in the message, and the default says the type but not the value. The typeAndValue setting puts the actual value into the message, which reads well for a friendly form error and is a bad idea for a password field or a token.

maxAllowedValuesToDisplay is a number defaulting to 10, and it caps how many allowed values are shown, with the rest hidden. allowedValuesSeparator defaults to a comma and a space, and allowedValuesLastSeparator defaults to the word or, with a note that setting it to undefined removes that final separator. Those two are small things, but they are the difference between a list that reads as a sentence and a list that reads as a data dump.

A deeper option is createMessageBuilder, exposed as its own API entry, which suggests the message construction is a separate, reusable layer rather than something hard coded inside the error map. Anyone who needs to change the wording itself, rather than the options, should read the full documentation.

error.details is the part you keep for yourself

The split between the user-facing message and the developer-facing detail is the design of the library, and it is worth being deliberate about which half you expose.

The message is the outer text of a ValidationError, and it is what toString returns. The details are the issues from the original Zod error, available on the error.details property. The README's motivation describes this as maintaining the original errors in an array for dev use, and the example in the API section prints the issues from a Zod error carried in the cause.

In practice this means a request handler can return validationError.toString() to the client while logging validationError.details on the server, where the code, the path and the parameters of the failure are still available for debugging. The trade is that the two views can drift apart, since the message is generated by the error map and the details are the untouched Zod issues.

One detail to keep in mind when designing that boundary. The reportInput option applies to the message, and typeAndValue will put the submitted value into it. Since the message is what goes to the client and is often what a client-side error tracker captures, that setting is a decision about data exposure, not about wording.

The v3 and v4 split is encoded in the package exports

The README carries a note that the current version of zod-validation-error works with Zod v4, and that v3 support is documented separately in README.v3.md. The feature list still advertises support for both Zod v3 and v4, which is accurate but needs unpacking, because the two live in different places in the package.

The package metadata shows the split. The main entry points at ./v4/index.js, ./v4/index.mjs and ./v4/index.d.ts, and the exports map adds two explicit subpaths:

json
{
  "exports": {
    ".": {
      "types": "./v4/index.d.ts",
      "require": "./v4/index.js",
      "import": "./v4/index.mjs"
    },
    "./v3": {
      "types": "./v3/index.d.ts",
      "require": "./v3/index.js",
      "import": "./v3/index.mjs"
    }
  }
}

So the default import gives you the v4 build, and a project still on Zod v3 imports the v3 build explicitly from the subpath. The published files array contains only the v3 and v4 directories, so both builds ship in the package even though you use one.

This is a sensible arrangement for a library whose upstream dependency had a breaking major version, and it is worth checking which import your project actually resolves, because a default import in a Zod v3 project silently brings the v4 build. The v3 and v4 releases are close together in time, with v3.5.4 and v5.0.0 both published on 2025-11-03, so the lines are maintained in parallel rather than one being abandoned.

MIT licensed, built with tsup, tested with vitest

The project is MIT licensed, and the package metadata gives a current version of 5.0.0 with the author listed as Dimitrios C. Michalakos and several contributors. The engines field requires node >= 18.0.0, matching the README's Node.js v.18+ requirement, and the package is marked sideEffects false, which lets a bundler drop the import when none of the exports are used.

The tooling is visible in the scripts. The build runs tsup with tsup.config.ts, the test command is vitest run, linting is eslint over lib, formatting is prettier, and the type check is tsc --noEmit. The repository also has a.husky directory, a.commitlintrc.cjs, a.changeset directory, a.nvmrc and a CHANGELOG.md, which is the conventional setup for a library that publishes with changesets and keeps a Node version file for contributors.

The release history has an odd shape. v4.0.2 was published on 2025-09-19, then v3.5.4 and v5.0.0 both on 2025-11-03. The last push to the repository was on 2026-10-19, so the code is still moving, and the changelog is where the details of what changed between those lines are recorded.

The practical upgrade question is the one the exports map answers. If your application is on Zod v4, you take the default import and stay on the v5 line. If it is on Zod v3, you keep the v3 subpath import and the v3 line, and you do not get the v4 fixes until you move Zod itself. The library is not forcing that migration on you, and the README does not say when the v3 line will be retired, so anyone on it should treat that as an open question rather than assume either way.

Editorial conclusion

zod-validation-error fits a TypeScript service that returns validation failures to end users and currently leaks raw Zod issues into API responses. It does not fit a codebase that only needs dev-time assertions, and it does not remove the need to think about what your users should be told, because the formatting options change the wording and the verbosity but not the policy. Before you adopt it, decide whether you want the error map configured globally through zod.config or the fromError wrapper at each boundary, and check the reportInput setting, because reporting the user input back can put sensitive values into logs and error trackers.

Frequently asked questions

How do I install zod-validation-error and what do I need?

Install it with npm install zod-validation-error. The README lists Node.js v.18+ and TypeScript v.4.5+ as requirements, and the package metadata declares node >= 18.0.0 in its engines field.

Does zod-validation-error work with Zod v3 as well as v4?

The current version targets Zod v4, and the README points to README.v3.md for v3 support. The package exports map keeps both, with the default import resolving to the v4 build and a ./v3 subpath for projects still on Zod v3.

How do I keep the original Zod errors for debugging?

The original issues are preserved on the error.details property of the ValidationError, and the README describes the arrangement as maintaining the original errors in an array for dev use. A ValidationError can also carry the original Zod error in the standard cause option.

What does the reportInput option do in zod-validation-error?

It controls whether the user input appears in the error message. It accepts false, type or typeAndValue, and defaults to type, which reports the type of the input but not its value.

Do I have to use the zod.config error map with zod-validation-error?

No. The README states that configuring the error map is optional, since you may also use your own custom error map or Zod's native one, and the fromError wrapper can be used on its own at the point where you catch the error.

Official sources

  1. causaly/zod-validation-error on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/causaly-zod-validation-error.svg)](https://hysenlabs.com/projects/causaly-zod-validation-error)

Was this useful?