zod icon indicating copy to clipboard operation
zod copied to clipboard

Custom error map not working

Open lekterable opened this issue 3 years ago • 3 comments

Hello, I'm trying to customize the error messages using an error map but it doesn't seem to have any effect, the function gets executed, but the returned message doesn't come from the map.

Example:

const customErrorMap = (issue, ctx) => {
  return { message: "custom" };
};

const email = z.string({ required_error: "required" }).email("invalid");

const res = email.safeParse("email", { errorMap: customErrorMap });

I would expect res to contain the custom error message, but in fact it returns: Screenshot 2022-07-04 at 13 59 40

If I remove .email("invalid") it works, so seems like the chained validation has a higher priority than the error map which should not be the case.

Repro: https://codesandbox.io/s/festive-gould-m7y3nk?file=/src/index.ts:22-297

lekterable avatar Jul 04 '22 16:07 lekterable

Yes, the default error map has the highest priority, no matter what you do...

There are a lot of issues like that. This behavior is also stopping me from creating good error messages in nestjs-zod library.

https://github.com/colinhacks/zod/issues/1043#issuecomment-1126859190

risenxxx avatar Jul 31 '22 21:07 risenxxx

Yes, the default error map has the highest priority, no matter what you do...

If there's a scenario where the default map takes precedence over one passed into .parse please file an issue with a minimal reproduction. This isn't the problem described by OP.

the returned message doesn't come from the map

In this scenario, the "hard coded" error you passed into .email and z.string({ ... }) take preference over the contextual one. That's intentional and makes sense to me. If you want to override those error messages why are you hard-coding them into your schema?

colinhacks avatar Sep 06 '22 08:09 colinhacks

@colinhacks I need my error messages localized so I'm hardcoding the i18n keys and I was hoping to use .safeParse later once the t() function is ready to get the actual messages.

Do you have some better example of doing this? I didn't find any mention of internationalizing the messages in the docs, thanks

lekterable avatar Sep 30 '22 05:09 lekterable

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.

stale[bot] avatar Nov 29 '22 05:11 stale[bot]

@colinhacks I have the exact same use case, translating "hardcoded" messages.

z.setErrorMap((issue, ctx) => {
  if (!issue.message) {
    return ctx.defaultError;
  }

  return {
    message: i18n._(issue.message),
  };
});

Unfortunately, this does not work, but really should.

This is a really convenient way to translate error messages when your message content is the key.

ifeltsweet avatar Feb 01 '23 14:02 ifeltsweet

I'm doing the same thing, and running into the same problem. The issue is this line:

message: issueData.message || errorMessage

Which clearly deprioritizes the message produced by my custom error map. So frustrating.

ivanjonas avatar Mar 23 '23 22:03 ivanjonas

I also faced this issue when dealing with i18n. I solved it by not specifying a message at all and instead pass the key through params to then intercept it in the error map:

const register = z.object({
  email: z.string().email().trim(),
  password: z
    .string()
    .min(8)
    .max(250)
    // Use params instead of message
    .refine(minLowercase(1), (v) => ({
      params: {
        i18n: "errors.custom.minLowercase", // "Should at least contain {{count}} lowercase character(s)"
        count: 1,
      },
    }))
});

// Intercept "custom" issue and use the t function to translate it

z.setErrorMap((issue, ctx) => {
  let message = ctx.defaultError;

  switch (issue.code) {
    case "custom":
      if (issue.params && "i18n" in issue.params) {
        message = t(issue.params.i18n, { values: issue.params });
      }
  }

  return { message };
});

// Parse the schema and check error

JSON.stringify(register.safeParse({ email: "[email protected]", password: "MYPASSWORD" }))

/*

Looks good ✅

{
  "code": "custom",
  "params": {
    "i18n": "errors.custom.minLowercase",
    "count": 1
  },
  "path": ["password"],
  "message": "Should at least contain 1 lowercase character(s)"
}

*/

ajmnz avatar Mar 25 '23 20:03 ajmnz