Пользовательские обработчики ошибок

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


При неуспешной валидации Zod возвращает экземпляр ZodError, внутри которого находится массив issues. Каждый элемент описывает отдельную ошибку:

  • path — путь к полю в структуре данных
  • message — текстовое описание ошибки
  • code — тип ошибки (например, invalid_type, too_small, custom)
  • дополнительные поля, зависящие от типа ошибки

Пример структуры:

import { z } from "zod";

const schema = z.object({
  age: z.number(),
});

try {
  schema.parse({ age: "not a number" });
} catch (e) {
  console.log(e);
}

Результирующий ZodError будет содержать issue вида:

{
  path: ["age"],
  message: "Expected number, received string",
  code: "invalid_type"
}

Такой формат позволяет легко строить пользовательские интерфейсы ошибок и API-ответы без дополнительного парсинга строк.


Глобальная настройка сообщений через errorMap

Zod предоставляет механизм централизованного управления сообщениями об ошибках через z.setErrorMap.

errorMap — функция, которая получает контекст ошибки и возвращает объект с сообщением:

import { z } from "zod";

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

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

Контекст ctx содержит:

  • defaultError — стандартное сообщение Zod
  • data — значение, которое не прошло проверку
  • path — путь до поля

Такой подход позволяет централизованно переопределять текст ошибок без изменения схем.


Локальные сообщения в схемах

Помимо глобального механизма, сообщения можно задавать прямо в месте определения схемы:

const schema = z.object({
  email: z.string({
    required_error: "Email обязателен",
    invalid_type_error: "Email должен быть строкой",
  }),
});

Для базовых типов также поддерживаются встроенные методы:

const age = z.number().min(18, { message: "Возраст должен быть не менее 18" });

Это позволяет комбинировать глобальные и локальные стратегии обработки ошибок.


refine и superRefine как источник пользовательских ошибок

Методы refine и superRefine позволяют создавать полностью кастомные правила валидации.

refine

Используется для простых проверок:

const schema = z.string().refine((val) => val.startsWith("A"), {
  message: "Строка должна начинаться с A",
});

superRefine

Позволяет формировать сложные ошибки с точным указанием пути:

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

superRefine особенно важен при проверке взаимосвязанных полей.


Методы форматирования ошибок: format и flatten

Zod предоставляет два удобных метода преобразования ошибок в более пригодный для UI вид.

flatten

Преобразует ошибки в плоскую структуру:

const result = schema.safeParse(data);

if (!result.success) {
  console.log(result.error.flatten());
}

Выход:

{
  formErrors: [],
  fieldErrors: {
    age: ["Expected number, received string"]
  }
}

format

Создаёт вложенную структуру, повторяющую схему данных:

result.error.format();

Пример результата:

{
  age: {
    _errors: ["Expected number, received string"]
  }
}

Разница между ними заключается в способе представления: flatten удобен для форм, format — для сложных вложенных структур.


Кастомизация через преобразование ZodError

Для интеграции с API часто требуется привести ошибки к единому формату.

Пример преобразования:

function toApiErrors(error: z.ZodError) {
  return error.issues.map((issue) => ({
    field: issue.path.join("."),
    message: issue.message,
    type: issue.code,
  }));
}

Результат удобно использовать в REST или GraphQL ответах.


Локализация ошибок (i18n подход)

Zod не содержит встроенной системы локализации, однако errorMap позволяет реализовать её вручную:

const messages = {
  ru: {
    invalid_type: "Неверный тип данных",
    too_small: "Слишком маленькое значение",
  },
  en: {
    invalid_type: "Invalid type",
    too_small: "Value is too small",
  },
};

z.setErrorMap((issue, ctx) => {
  const lang = "ru";

  const msg = messages[lang][issue.code];

  return {
    message: msg || ctx.defaultError,
  };
});

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


Управление ошибками на уровне парсинга

Методы parse и safeParse определяют поведение обработки ошибок:

  • parse — выбрасывает исключение ZodError
  • safeParse — возвращает объект { success, data, error }

Пример безопасного подхода:

const result = schema.safeParse(input);

if (!result.success) {
  const errors = result.error.issues;
}

Это позволяет централизованно управлять потоком выполнения без try/catch.


Работа с кодами ошибок (ZodIssueCode)

Каждая ошибка имеет код, который определяет её тип:

  • invalid_type
  • too_small
  • too_big
  • custom
  • invalid_literal
  • unrecognized_keys

Использование кода позволяет строить условную обработку:

z.setErrorMap((issue, ctx) => {
  switch (issue.code) {
    case "too_small":
      return { message: "Значение ниже допустимого" };

    case "invalid_type":
      return { message: "Неверный тип" };

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

Интеграция пользовательских обработчиков в архитектуру приложения

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

Пример промежуточного слоя:

function handleValidationError(err: z.ZodError) {
  return {
    status: "validation_error",
    fields: err.issues.reduce((acc, issue) => {
      const key = issue.path.join(".");
      acc[key] = issue.message;
      return acc;
    }, {} as Record<string, string>),
  };
}

Такой слой отделяет валидацию от бизнес-логики и упрощает поддержку API.


Комбинирование refine и errorMap

При совместном использовании кастомных проверок и глобальных сообщений важно учитывать приоритет:

  1. Сообщение из refine/superRefine имеет приоритет
  2. Локальные сообщения схемы
  3. errorMap
  4. ctx.defaultError

Пример:

const schema = z.string().refine((v) => v.length > 3, {
  message: "Слишком короткая строка",
});

В этом случае errorMap не переопределит заданное сообщение, если оно уже явно указано.


Обработка множественных ошибок в сложных структурах

При валидации вложенных объектов Zod собирает ошибки рекурсивно:

const schema = z.object({
  user: z.object({
    name: z.string(),
    age: z.number(),
  }),
});

Ошибки будут иметь путь:

["user", "name"]
["user", "age"]

Это позволяет строить точечную визуализацию ошибок на уровне форм и интерфейсов.


Контроль формата сообщений на уровне проекта

В крупных проектах часто вводится единый стандарт обработки ошибок Zod:

  • централизованный errorMap
  • утилита преобразования ZodError
  • единый API формат
  • разделение системных и пользовательских сообщений

Пример стандартизированного преобразования:

function normalizeZodError(error: z.ZodError) {
  return {
    code: "VALIDATION_ERROR",
    details: error.issues.map((i) => ({
      path: i.path,
      message: i.message,
      type: i.code,
    })),
  };
}

Такая модель упрощает интеграцию с фронтендом и микросервисами.


Поведение при union-типы и альтернативных схемах

При использовании z.union ошибки могут агрегироваться из нескольких веток:

const schema = z.union([
  z.string(),
  z.number(),
]);

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