zod
zod copied to clipboard
Custom error map not working
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:

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
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
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 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
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.
@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.
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.
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)"
}
*/