zod: Allow Custom Errors on any value?

Any reason not to allow defining the last argument for any value as an error message to allow overriding the error message for that property easily? The current method is not really ideal, especially when you have many string props but need different errors.

A couple nice ways to handle it:

Allow Last Param to be Error Message

Would also accept a custom error that it would throw instead of ZodError (similar to Joi)

const TestSchema = z.object({
	three: z.literal('hi').optional(),
	one: z.number(),
	two: z.literal(3, 'that isnt 3 dummy'),
	four: z.string('string not whatever you gave!'),
	five: z.date()
})

Add a .error() option like Joi

const TestSchema = z.object({
	three: z.literal('hi').optional(),
	one: z.number(),
	two: z.literal(3).error(new Error('that isnt 3 dummy')),
	four: z.string().optional().error(new Error('string not whatever you gave!')),
	five: z.date()
})

For the VSCode extension I am working on, these styles add the benefit that jsdoc can be used to transfer over error messaging easily:

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 35
  • Comments: 32 (9 by maintainers)

Most upvoted comments

This is on the roadmap. Though it’ll likely be an object instead of a string to leave room for future API additions.

z.string({ message: "That's not a string!" });

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

What about yup#setLocale API? It seems to be easier to handle global translations for both primitives and their methods

Zod brought me out of two days of depression after going through several other libraries.

At the moment this is the only problem I have encountered. Against the background of everything else, this is a mere trifle, but nevertheless I am also waiting for a solution to this problem.

Zod is really super cool, but I also stumbled upon this issue here while figuring out how to use React Hook Form resolvers together with localized error messages using i18next. The API with a message property (like z.string().min(5, { message: "Must be 5 or more characters long" }) is quite ok, but it must be possible to pass in an object as a message. Otherwise, it won’t be possible to translate complex messages with interpolations in them. Is this still planned?

I’m also looking forward to this - I’m using zod in forms, and if a user fails to enter a required field, i’d rather it say something like “X is required” instead of Expected number, received nan, but at the moment I don’t see a good way to add that requiremnet.

Hmmm just stumbled upon a usecase where we need a custom error for invalid_literal values when checking literals. 😕

Let me share my latest workaround for this (updated since May 17):

// file: zod-translation.ts
import i18n, { TOptions } from "i18next";

class TranslatedString extends String {
  private readonly restArgs: (TOptions | string)[];

  constructor(private readonly key: string, ...restArgs: (TOptions | string)[]) {
    // if we used something else than an empty string here,
    // we would see the string being transformed to an array of chars
    // (seen in IntelliJ debugger)
    super("");

    this.restArgs = restArgs;
  }

  toString(): string {
    return i18n.t(this.key, ...this.restArgs);
  }
}

/**
 * Use this for localization of zod messages.
 * The function has to be named `t` because it mimics the `t` function from `i18next`
 * thus our IDE provides code completion and static analysis of translation keys.
 */
export function t(
  key: string,
  options: { path: string | string[] } | Record<string, unknown> = {}
): { message: string; path?: string[] } {
  const message: string = (new TranslatedString(key, options) as unknown) as string;
  const { path } = options;

  if (path === undefined) return { message };

  return {
    message,
    path: typeof path === "string" ? [path] : (path as string[]),
  };
}

Example usage:

import { t } from "./zod-translation.ts";

export const ZValidEncryptionKey = z
  .string()
  .regex(/^[a-zA-Z0-9]*$/, t("error.ZValidEncryptionKey.noSpacesAllowed"))
  .nullable();

I think the issue with ZodErrorMap is theres often cases where you want a certain schema to return a different error for that specific schema. The limit of having only a message object in the granular case precludes you from having error objects returned by zod, for later parsing by an application to react to those errors. E.g. being able to return an object of an arbitary shape.

Ultimately, only being able to return a message string in the non-global case precludes you from having custom errors for a schema where you want to do i18n later or if you need to react to those errors based on some key in your application for whatever reason. Lets say if you wanted to provide a button to the user to help them fix a certain error type. Tbh, I do think these use cases are mostly Ui based.

I’d suggest what would solve this is something like:

const ZMySchema = z.object({
  name: z.string().nonempty().regex(/^[A-Za-z ]$/, ({path}) => ({code: MY_CUSTOM_CODE, path)),
});

I’ll add that you can workaround this now by serialising your custom error obj into the message key but thats obviously very dirty. You also dont have access to the path so have to repeat it in your error obj.

Also want to use the opportunity to express how great this library is. Its actually fantastic to be able to write a schema and infer the type for use in a form lib like Formik. Really great stuff that deserves attention.

Until I find a better solution, I implemented the following workaround in my code:

import i18n, { TFunction } from "i18next";

class TranslatedString extends String {
  toString(): string {
    return i18n.t(super.toString());
  }
}

/**
 * Use this for localization of zod messages.
 * The function has to be named `t` because it mimics the `t` function from `i18next`
 * thus our IDE provides code completion and static analysis of translation keys.
 */
export const t: TFunction = (key: string) => ({ message: new TranslatedString(key) });

The trick is to defer the evaluation of the t() function until the string is printed which could have also been achieved if zod accepted a function as a message.

What if the schema is defined on a per-form basis (e.g react-hook-form) and I need to localize error messages (react-i18next).

// schema definition: notice the translation using t("...")
const ZMySchema = z.object({
  name: z.string().nonempty().regex(/^[A-Za-z ]$/, t("users:form.validNameRequired")),
});

// now registering the schema to the form
const form = useForm({ resolver: zodResolver(ZMySchema) });

Similar to yup which have a community driven package: yup-locales

In my case given that the error happens at the number() stage I just want to be able to do this:

z.number({ message: "Make it a number, doofus!" });

or alternatively for more complicated circumstances,

z.number({ message: ({ issue, ctx }) => makeBadNumberErrorMessage({ issue, ctx }) })

To me this is a logical UX choice, but I haven’t been using Zod for that long so I could be missing something that makes this impractical.

I have created a PR #543 that your could set required or invalid messages to string, number and boolean. Would like to hear from you guys on ways to improve it.

I’ve been trying to create schema with i18n error messages. Ended up on this solution:

const schemaFactory = (t: TFunction): z.ZodType<ApiLoginRequest> =>
  z.object({
    email: z.string().email({ message: t('Please enter valid email') }),
    password: z
      .string()
      .min(MIN_PASSWORD_LENGTH, t('Minimum {{chars}} characters', { chars: MIN_PASSWORD_LENGTH }))
      .max(MAX_PASSWORD_LENGTH, t('Maximum {{chars}} characters', { chars: MAX_PASSWORD_LENGTH })),
  });

export default function LoginFormContainer() {
  const { t } = useTranslation();
  // memoize the schema to prevent recreating on each render
  const schema = useMemo(() => schemaFactory(t), [t]);
  const { register, handleSubmit, reset } = useForm({ defaultValues, resolver: zodResolver(schema) });

  // ...rest of your component

Hope it might help someone

As @maxArturo said, this has been mostly implemented for a long time now - whoops. Almost every schema creation function z.whatever supports an options object with the required_error and invalid_type_error options.

const name = z.string({
  required_error: "Name is required",
  invalid_type_error: "Name must be a string",
});

This is true for all schema types.

z.object({ asdf: z.string() }, { required_error: "asdf" });

You can set an error map globally with z.setErrorMap.

const myErrorMap: z.ZodErrorMap = (val, ctx) => {
  return { message: "whatever" };
};
z.setErrorMap(myErrorMap);

You can set an error map on a per-schema basis

z.string({ errorMap: myErrorMap })

And you can set a contextual error map in a particular call to .parse.

z.string().parse("tuna", {errorMap: myErrorMap})

i8n support for error messages

Tracked in other issues. #1622

Closing. If the set of current offering doesn’t cover a particular use case, another issue can be opened.

Hi all! This seems to be one of the oldest open issues in the repo, and the original header issue title seems to have been resolved some time ago with custom errors and errorMaps.

However there are some additional asks that seem very useful and probably merit their own issues. Would it be better to open separate issues on these topics and discuss/track progress there instead? I took a quick look but couldn’t find a great home for the following:

Let me know your thoughts! 🙏

Landed here while working on an issue today where my react app with i18next translated literals were not getting used. I tried @vsimko’s solution, but found it didn’t work with i18next-parser (which I’m using to extract translations). The root issue is that the schema is created before i18next gets initialized (I’m doing so in a hook in the root component). I found an easy alternative which is to just move the schema code and its type inside of the component:

//react functional component
export const LoginForm = () => {

    //zod schema with literal strings to be translated with i18next
    const schema = z.object({
    username: z.string()
      .nonempty(t`Email is required`)
      .email(),
    password: z.string()
      .nonempty(t`Password is required`)
      .min(8, t`Password must be at least 8 characters`)
  });
  
  type Schema = z.infer<typeof schema>;

  //react-hook-form useForm hook w/ zodResolver
  const {register, handleSubmit, formState: {errors}} = useForm<Schema>({
    resolver: zodResolver(schema)
  });
...rest of component
}

Previously, my schema object and type were above the LoginForm component itself and I only got the default messages. Now I have full type safety and localized validation messages, which is awesome. Hope this helps someone else.