Кастомные сообщения об ошибках

Валидация данных в Zod построена вокруг идеи декларативного описания схемы и получения структурированных ошибок. По умолчанию библиотека генерирует стандартизированные сообщения, но в реальных приложениях требуется адаптация текста под контекст: язык интерфейса, бизнес-логику, UX-требования, локализацию.

Кастомизация сообщений в Zod реализуется на нескольких уровнях: через параметры методов, через предикаты refine и superRefine, а также через глобальную настройку errorMap.


Локальные сообщения на уровне валидаторов

Большинство базовых валидаторов Zod поддерживают передачу пользовательского сообщения вторым аргументом.

Строковые ограничения

import { z } from "zod";

const schema = z.string()
  .min(5, { message: "Минимальная длина строки — 5 символов" })
  .max(20, { message: "Слишком длинная строка" });

Каждый метод проверки принимает объект с ключом message, который переопределяет стандартный текст ошибки.

Аналогично работает для чисел:

const ageSchema = z.number()
  .min(18, { message: "Возраст должен быть не менее 18 лет" })
  .max(65, { message: "Возраст превышает допустимый предел" });

Кастомизация через refine

Метод refine используется для добавления пользовательской логики проверки. Он позволяет задать условие и сообщение ошибки одновременно.

const passwordSchema = z.string().refine(
  (value) => value.includes("#"),
  { message: "Пароль должен содержать символ #" }
);

Особенность refine заключается в том, что ошибка привязывается к конкретному полю и не раскрывает внутреннюю логику проверки, что полезно для безопасности и UX.


Множественные ошибки через superRefine

Для более сложных сценариев используется superRefine, позволяющий регистрировать несколько ошибок вручную через ctx.addIssue.

const schema = z.object({
  password: z.string(),
  confirmPassword: z.string(),
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      path: ["confirmPassword"],
      message: "Пароли не совпадают",
      code: z.ZodIssueCode.custom,
    });
  }

  if (data.password.length < 8) {
    ctx.addIssue({
      path: ["password"],
      message: "Пароль слишком короткий",
      code: z.ZodIssueCode.custom,
    });
  }
});

Здесь важно, что path определяет точное поле, к которому привязывается ошибка, а code позволяет классифицировать тип проблемы.


Глобальная настройка errorMap

Для централизованного управления сообщениями используется механизм errorMap. Он позволяет переопределить стандартные тексты для всех схем сразу.

import { z } from "zod";

z.setErrorMap((issue, ctx) => {
  switch (issue.code) {
    case z.ZodIssueCode.invalid_type:
      return { message: "Неверный тип данных" };

    case z.ZodIssueCode.too_small:
      return { message: "Значение слишком маленькое" };

    default:
      return { message: ctx.defaultError };
  }
});

Функция получает:

  • issue — объект ошибки с кодом и метаданными
  • ctx — контекст с дефолтным сообщением

Такой подход особенно полезен при локализации или унификации сообщений во всём приложении.


Контекстные сообщения через errorMap в схеме

Помимо глобального setErrorMap, Zod позволяет задавать errorMap на уровне конкретной схемы.

const schema = z.string({
  errorMap: () => ({ message: "Ошибка в строковом поле" })
});

Это переопределение имеет приоритет над глобальной настройкой, но ниже приоритета локальных сообщений в методах (min, max, refine и т.д.).


Использование контекста ctx в errorMap

errorMap может учитывать дополнительный контекст через ctx.data, что полезно при динамических сообщениях.

const schema = z.number({
  errorMap: (issue, ctx) => {
    if (ctx.data?.role === "admin") {
      return { message: "Администратору это значение недоступно" };
    }
    return { message: "Некорректное число" };
  }
});

Такой подход позволяет учитывать состояние приложения при формировании ошибки.


Приоритеты сообщений об ошибках

Zod использует строгую иерархию при выборе сообщения:

  1. Сообщение в refine / superRefine (ctx.addIssue)
  2. Сообщение в параметрах метода (min, max, regex)
  3. errorMap на уровне схемы
  4. Глобальный setErrorMap
  5. Стандартное сообщение Zod

Понимание этого порядка критично при проектировании сложных схем валидации.


Кастомизация ошибок для объектов и вложенных структур

Вложенные схемы наследуют поведение сообщений, но каждое поле может иметь собственную настройку.

const schema = z.object({
  user: z.object({
    name: z.string().min(1, { message: "Имя обязательно" }),
    email: z.string().email({ message: "Некорректный email" }),
  }),
});

Ошибки в этом случае формируют структурированный массив с путями:

  • user.name
  • user.email

Это позволяет точно отображать сообщения в UI-формах.


Формирование пользовательских структур ошибок

Zod возвращает ошибки в формате ZodError, содержащем массив issues. Каждый элемент включает:

  • path — путь к полю
  • message — текст ошибки
  • code — тип ошибки
  • expected / received — при типовых ошибках

На основе этого массива часто строится собственная система отображения сообщений:

try {
  schema.parse(data);
} catch (e) {
  if (e instanceof z.ZodError) {
    const formatted = e.issues.map((i) => ({
      field: i.path.join("."),
      error: i.message,
    }));
  }
}

Локализация сообщений

Кастомные сообщения часто используются как основа для мультиязычных систем. Подход заключается в передаче не строк, а ключей:

const schema = z.string().min(5, {
  message: "validation.string.min"
});

Далее слой приложения интерпретирует ключи в зависимости от текущей локали.

Альтернативный вариант — динамическая генерация через errorMap:

const messages = {
  ru: {
    too_small: "Слишком мало символов",
  },
  en: {
    too_small: "Too few characters",
  },
};

Ограничения кастомизации

Несмотря на гибкость, существует несколько ограничений:

  • нельзя полностью изменить структуру ZodError без обёрток
  • refine не позволяет разделять несколько независимых ошибок без superRefine
  • глобальный errorMap не различает контекст формы без дополнительных данных
  • сложные правила могут усложнять читаемость схем

Эти ограничения компенсируются явной структурой и предсказуемостью поведения библиотеки