Валидация данных в 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 используется для добавления
пользовательской логики проверки. Он позволяет задать условие и
сообщение ошибки одновременно.
const passwordSchema = z.string().refine(
(value) => value.includes("#"),
{ message: "Пароль должен содержать символ #" }
);
Особенность refine заключается в том, что ошибка
привязывается к конкретному полю и не раскрывает внутреннюю логику
проверки, что полезно для безопасности и UX.
Для более сложных сценариев используется 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. Он позволяет переопределить стандартные тексты
для всех схем сразу.
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 — контекст с дефолтным сообщениемТакой подход особенно полезен при локализации или унификации сообщений во всём приложении.
Помимо глобального setErrorMap, Zod позволяет задавать
errorMap на уровне конкретной схемы.
const schema = z.string({
errorMap: () => ({ message: "Ошибка в строковом поле" })
});
Это переопределение имеет приоритет над глобальной настройкой, но
ниже приоритета локальных сообщений в методах (min,
max, refine и т.д.).
errorMap может учитывать дополнительный контекст через
ctx.data, что полезно при динамических сообщениях.
const schema = z.number({
errorMap: (issue, ctx) => {
if (ctx.data?.role === "admin") {
return { message: "Администратору это значение недоступно" };
}
return { message: "Некорректное число" };
}
});
Такой подход позволяет учитывать состояние приложения при формировании ошибки.
Zod использует строгую иерархию при выборе сообщения:
refine / superRefine
(ctx.addIssue)min, max,
regex)errorMap на уровне схемыsetErrorMapПонимание этого порядка критично при проектировании сложных схем валидации.
Вложенные схемы наследуют поведение сообщений, но каждое поле может иметь собственную настройку.
const schema = z.object({
user: z.object({
name: z.string().min(1, { message: "Имя обязательно" }),
email: z.string().email({ message: "Некорректный email" }),
}),
});
Ошибки в этом случае формируют структурированный массив с путями:
user.nameuser.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 не позволяет разделять несколько независимых
ошибок без superRefineerrorMap не различает контекст формы без
дополнительных данныхЭти ограничения компенсируются явной структурой и предсказуемостью поведения библиотеки