Support for Schema Instance Parameter in Zod Global customError and localeError?
In Zod v4, the global configuration method z.config allows customizing error messages via the customError and localeError function, which currently only receives the issue parameter. The requirement is for customError localeError to support a second parameter, which provides the actual schema instance responsible for the validation error. This enables direct access to the meta information (such as title, label, etc.) attached to that schema instance and allows developers to implement consistent localized error handling logic.
Key Points:
customError(issue, schemaInstance): the second parameter is the schema instance where the error occurred, localeError accepts the same parameters. Works for both base and chained schemas (e.g., min/max/refine), always providing the schema with its associated meta. Compatible with localization and meta-driven error formatting, enabling easier access to meta fields like title or label for custom error messages.
z.config({
localeError,
customError(issue, schemaInstance) {
const title = schemaInstance.meta()?.title;
const message = localeError(issue, schemaInstance);
// ...
return title ? `${title} - ${message}` : message;
}
})
Expected Benefits:
Simplifies multilingual and meta-driven error handling. Resolves the problem of losing meta information in chained validations. Makes Zod’s error handling API more consistent and easier to use for localization and customization scenarios.
Using issue.inst is not safe, as it may not necessarily point to schemaInstance.
Zod v4's global error customization functions, such as customError and localeError in z.config, only receive a single issue parameter and do not support a second schemaInstance parameter(1)(2). The recommended way to access schema meta information during error customization is through issue.inst.meta(), but this is only available during error mapping and not after error finalization(3). There is no documented support for passing the schema instance as a second parameter to customError or localeError. Your proposed API with customError(issue, schemaInstance) is not currently available in Zod v4(1)(2)(3).
📚 Sources:
Have another question? Just tag @inkeep.
Ran into the same issue today - we currently ship Zod errors to users, but getting an error about a "string" being too long when it's the "title" that's too long is confusing to them. There aren't very good alternative routes to making this happen:
- Adding custom formatters to each
.min()and.max()check is repetitive - It would be possible to look through
z.globalRegistryand create a map likeMap<ZodCheck, schema>and be able to lookup the schema from the check and then the meta from the schema, butz.globalRegistryis a WeakMap so that isn't possible.
I'm attempting to convert from yup to zod, and really struggling with localization and custom errors. I simply want to have a localized error message with a label in it. I'm probably just missing something because feels like a common scenario.
For example:
const schema = zod.string()
const output = schema.safeParse(undefined);
// Invalid input: expected string, received undefined
I get that the error message is localized, but since this is in a form, the error would be better as Name is a required field.
The workaround I have is to attach meta to the schema:
const schema = zod.string().meta({label: 'Name'});
const output = schema.safeParse(undefined);
export const zodErrorMap = (t: TFunction): z.ZodErrorMap<z.core.$ZodIssue> => {
return (issue: z.core.$ZodRawIssue) => {
const label = (issue.inst as any)?.meta()?.label;
if (issue.code === 'invalid_type') {
if (label) {
return t('{{label}} is a required field', { label });
}
}
return issue.message;
};
};
z.config({
customError: zodErrorMap(t),
});
Is this the right way to do this? Feels a bit hacky.
I'm struggling with the same question, how to change string by Name in an error message (e.g. Too small: expected string to have >=3 characters).
I run
const schema = zod.string().min(3).meta({ label: 'Name' })
const output = schema.safeParse(undefined)
and I have issue.inst is instance of ZodString and can call issue.inst.meta()
But when I run
const schema = zod.string().min(3).meta({ label: 'Name' })
const output = schema.safeParse('AB')
issue.inst is instance of $ZodCheckMinLength and calling issue.inst.meta() output TypeError: issue.inst.meta is not a function
update 2025-11-14:
I found a workaround that works for me. Since I'm using zod with react-hook-form, I just created a simple wrapper for zodResolver.
export function customZodResolver (schema, schemaOptions = {}, resolverOptions = undefined) {
return zodResolver(schema, {
...schemaOptions,
error: iss => customZodErrorHandler(schema, iss)
}, resolverOptions)
}
@cgatian I attempted a similar approach.
Unfortunately, I found that schema metadata is not accessible inside the errorMap for validation errors like min or max.
This means I cannot implement a global handler like this:
import z from 'zod'
type Meta = {
locale: 'zh-CN' | 'en-US' | 'ja-JP'
title: string
description: string
}
const customErrorMap: z.core.$ZodErrorMap = (issue) => {
const inst = issue.inst
if (!inst) return
if ('meta' in inst && typeof inst.meta === 'function') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const { title } = inst.meta() as Meta
if (issue.code === 'invalid_type') { // Works: instance refers to the schema itself
return { message: `${title} can not be empty` }
}
if (issue.code === 'too_small' && issue.origin === 'string') { // Fails: instance refers to the Check object
return {
message: `${title} must be at least ${issue.minimum} characters`,
}
}
if (issue.code === 'too_big' && issue.origin === 'string') { // Fails: instance refers to the Check object
return { message: `${title} must be at most ${issue.maximum} characters` }
}
}
}
export function initZod() {
z.config({ customError: customErrorMap })
}
I use drizzle-zod, which automatically generates schema constraints (e.g. max(32) for varchar({ length: 32 })).
Currently, I have to override it manually:
const test = createInsertSchema(productCategoryTable, {
name: z
.string()
.min(1, { error: 'product category name must be at least 1 character' })
.max(32, { error: 'product category name must be at most 32 characters' })
})
If the errorMap had access to the schema metadata, I could simplify it to:
const test = createInsertSchema(productCategoryTable, {
name: (schema) => schema.meta({ title: 'name' }),
})
While I can write helper functions to wrap this logic:
type Param = { title: string; min?: number; max?: number }
const zText = ({ title, min, max }: Param) => {
let text = z.string()
if (min)
text = text.min(min, {
error: `${title} must be at least ${min} characters`,
})
if (max)
text = text.max(max, {
error: `${title} must be at most ${max} characters`,
})
return text
}
It doesn't really reduce the boilerplate significantly compared to the ideal solution.