Форматирование ошибок

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

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


Каждый объект в issues содержит набор стандартных полей:

  • code — тип ошибки
  • path — путь к значению в проверяемой структуре
  • message — текстовое описание ошибки
  • expected / received — ожидаемое и фактическое значение (для части типов)
  • unionErrors — вложенные ошибки при union-валидации

Пример базового объекта:

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

Поле path является ключевым элементом системы форматирования. Оно представляет путь в виде массива ключей и индексов, позволяя точно локализовать источник ошибки:

{
  path: ["user", "address", "zip"]
}

Типы ошибок и их влияние на форматирование

Внутренний механизм Zod классифицирует ошибки по коду code, что определяет структуру дополнительных полей:

  • invalid_type — несоответствие типа данных
  • invalid_literal — значение не совпадает с литералом
  • too_small — значение меньше допустимого
  • too_big — значение больше допустимого
  • invalid_union — ошибка объединённого типа
  • custom — пользовательская ошибка

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


Базовое форматирование через ZodError.toString

Метод toString() формирует строковое представление всех ошибок:

const result = schema.safeParse(data);

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

Формат вывода агрегирует все issues в последовательность строк, но не структурирует их по вложенности, что делает его пригодным только для отладки.


flatten(): плоское представление ошибок

Метод flatten() преобразует дерево ошибок в плоскую структуру:

const result = schema.safeParse(data);

if (!result.success) {
  const flat = result.error.flatten();
}

Результат имеет форму:

{
  formErrors: [],
  fieldErrors: {
    username: ["Required"],
    "address.city": ["Invalid value"]
  }
}

Особенности:

  • formErrors содержит ошибки без пути
  • fieldErrors группирует ошибки по ключам верхнего уровня
  • вложенные структуры сериализуются в строки путей

Такой формат широко используется в UI-слое, поскольку позволяет напрямую привязывать ошибки к полям формы.


format(): древовидное форматирование

Метод format() сохраняет структуру данных и отображает ошибки в виде вложенного дерева:

const result = schema.safeParse(data);

if (!result.success) {
  const formatted = result.error.format();
}

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

{
  username: {
    _errors: ["Required"]
  },
  address: {
    city: {
      _errors: ["Invalid city"]
    }
  }
}

Особенности:

  • структура повторяет форму входных данных
  • каждая ветка содержит _errors
  • удобно для глубоко вложенных объектов

Обработка union-ошибок

При использовании z.union() структура ошибок становится вложенной:

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

При несоответствии возникает invalid_union, содержащий массив unionErrors:

{
  code: "invalid_union",
  unionErrors: [ZodError, ZodError]
}

Каждый элемент unionErrors представляет отдельную попытку проверки альтернативной схемы.

Форматирование таких ошибок требует рекурсивного обхода:

  • каждая ветка анализируется отдельно
  • результаты объединяются или выбирается наиболее релевантный вариант
  • возможно агрегирование сообщений

Кастомизация сообщений через errorMap

Система форматирования поддерживает глобальную настройку через setErrorMap:

import { z } from "zod";

z.setErrorMap((issue, ctx) => {
  return { message: "Ошибка валидации" };
});

Параметры callback:

  • issue — объект ошибки
  • ctx — контекст (включая дефолтное сообщение)

Пример более сложной логики:

z.setErrorMap((issue, ctx) => {
  if (issue.code === "invalid_type") {
    return { message: `Ожидался ${issue.expected}` };
  }
  return { message: ctx.defaultError };
});

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


Локальная кастомизация через refine и superRefine

Ошибки, созданные вручную, имеют код custom:

z.string().refine((val) => val.length > 3, {
  message: "Слишком короткое значение"
});

В superRefine возможна генерация нескольких ошибок:

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

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


Сериализация ошибок для внешних систем

При передаче ошибок через API часто используется ручное преобразование:

const result = schema.safeParse(data);

if (!result.success) {
  const serialized = result.error.issues.map(issue => ({
    path: issue.path.join("."),
    message: issue.message,
    code: issue.code
  }));
}

Такой формат:

  • упрощает JSON-ответ
  • устраняет вложенность
  • облегчает обработку на клиенте

Нормализация путей ошибок

Путь path может содержать:

  • строки (ключи объектов)
  • числа (индексы массивов)

Пример:

["users", 0, "email"]

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

users.0.email

Это упрощает:

  • поиск в UI-деревьях
  • связывание с DOM-элементами
  • логирование

Группировка ошибок по уровням вложенности

При сложных схемах полезно выполнять агрегацию:

  • уровень 1: корневые поля
  • уровень 2: вложенные объекты
  • уровень 3+: массивы и динамические структуры

Такое представление позволяет:

  • выделять критические ошибки верхнего уровня
  • скрывать второстепенные детали
  • формировать компактные отчёты

Особенности форматирования в массивах

Ошибки внутри массивов используют числовые индексы:

users: [
  {
    name: "Valid"
  },
  {
    name: 123
  }
]

Результирующий путь:

["users", 1, "name"]

Форматирование может преобразовывать такие ошибки в:

users[1].name

Поведение при множественных ошибках одного поля

Если поле содержит несколько нарушений, issues содержит несколько записей с одинаковым path.

Пример:

  • слишком короткое значение
  • недопустимые символы

Форматирование:

  • либо объединяет сообщения
  • либо выводит массив сообщений
  • либо оставляет отдельные записи

Влияние трансформаций (transform) на ошибки

При использовании transform() ошибки возникают до или после преобразования в зависимости от цепочки:

z.string()
  .transform(val => Number(val))

Если исходное значение не строка, ошибка фиксируется на этапе входа и не доходит до трансформации.


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

Форматирование часто дополняется переводом сообщений через errorMap:

  • замена стандартных английских сообщений
  • внедрение доменной терминологии
  • адаптация под интерфейс приложения

Пример:

Expected string → Ожидалась строка
Required → Обязательное поле

Использование валидации как слоя контрактов

Форматирование ошибок в Zod фактически определяет контракт между слоями системы:

  • backend формирует структурированные issues
  • frontend интерпретирует path и message
  • API передаёт сериализованный набор ошибок

Структура ZodError обеспечивает единый формат взаимодействия независимо от сложности схемы и глубины вложенности данных