zod icon indicating copy to clipboard operation
zod copied to clipboard

Support for Schema Instance Parameter in Zod Global customError and localeError?

Open nuintun opened this issue 3 months ago • 5 comments

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.

nuintun avatar Sep 15 '25 09:09 nuintun

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:

  1. Customizing errors
  2. Zod v4 LLMs-Full
  3. meta title or label inside z.config customError (zod 4)

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.globalRegistry and create a map like Map<ZodCheck, schema> and be able to lookup the schema from the check and then the meta from the schema, but z.globalRegistry is a WeakMap so that isn't possible.

tmcw avatar Oct 22 '25 14:10 tmcw

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.

cgatian avatar Nov 07 '25 14:11 cgatian

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)
}

wholegroup avatar Nov 10 '25 13:11 wholegroup

@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.

jianxingxuejian avatar Dec 01 '25 10:12 jianxingxuejian