Обработка ошибок валидации

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

Каждая ошибка внутри ZodError представлена как ZodIssue и содержит стандартизированную структуру:

  • code — тип ошибки (invalid_type, too_small, unrecognized_keys и т.д.)
  • path — путь к полю, где произошла ошибка
  • message — человекочитаемое описание
  • дополнительные поля, зависящие от типа ошибки (expected, received, minimum, maximum и т.д.)

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

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

Поведение parse и safeParse при ошибках

Zod предоставляет два основных способа выполнения валидации:

parse

Метод parse выбрасывает исключение при первой же ошибке валидации:

schema.parse(data)

При ошибке генерируется ZodError, который необходимо обрабатывать через try/catch:

try {
  schema.parse(data)
} catch (err) {
  if (err instanceof ZodError) {
    // обработка ошибок
  }
}

safeParse

Метод safeParse возвращает структурированный результат без исключений:

const result = schema.safeParse(data)

Форма результата:

{
  success: true,
  data: ...
}

или

{
  success: false,
  error: ZodError
}

Такой подход позволяет централизованно обрабатывать ошибки без использования исключений:

const result = schema.safeParse(data)

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

Работа с массивом issues

Поле issues в ZodError является основным источником информации для диагностики ошибок. Оно содержит все найденные несоответствия, включая вложенные структуры.

Каждый элемент массива содержит path, который отражает маршрут до проблемного поля:

[
  {
    path: ["user", "address", "zip"],
    message: "Invalid zip code"
  }
]

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

  • объекты: ["user", "profile", "email"]
  • массивы: ["items", 2, "price"]

Форматирование ошибок через format()

Метод format() преобразует ZodError в древовидную структуру, удобную для UI-отображения:

const formatted = error.format()

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

{
  user: {
    name: {
      _errors: ["Required"]
    }
  }
}

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

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

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

Метод flatten() преобразует ошибки в две группы:

const flat = error.flatten()

Результат:

{
  formErrors: [],
  fieldErrors: {
    name: ["Required"],
    email: ["Invalid email"]
  }
}

Использование:

  • formErrors — ошибки уровня всей формы
  • fieldErrors — ошибки конкретных полей

Такой формат часто применяется в UI-библиотеках, где требуется простая мапа поле → ошибка.


Индивидуальная обработка ZodIssue

Каждый ZodIssue содержит код, позволяющий классифицировать ошибку:

Основные типы:

  • invalid_type
  • too_small
  • too_big
  • invalid_string
  • unrecognized_keys
  • invalid_union
  • custom

Пример обработки:

for (const issue of error.issues) {
  switch (issue.code) {
    case "invalid_type":
      // обработка типа
      break
    case "too_small":
      // обработка минимального значения
      break
  }
}

Вложенные схемы и накопление ошибок

При работе с объектами и массивами Zod сохраняет точный путь до каждой ошибки. Например:

const schema = z.object({
  user: z.object({
    posts: z.array(
      z.object({
        title: z.string()
      })
    )
  })
})

Ошибка может выглядеть так:

{
  path: ["user", "posts", 0, "title"],
  message: "Required"
}

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


Ошибки union-типов

При использовании z.union() ошибки становятся агрегированными, так как проверяются несколько альтернативных схем.

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

При несоответствии всех вариантов формируется invalid_union:

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

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


superRefine и ручное добавление ошибок

Метод superRefine позволяет добавлять собственные ошибки в процессе валидации:

z.object({
  password: z.string()
}).superRefine((data, ctx) => {
  if (data.password.length < 8) {
    ctx.addIssue({
      code: "custom",
      message: "Too short",
      path: ["password"]
    })
  }
})

Контекст ctx предоставляет:

  • addIssue() — добавление ошибки
  • доступ к данным всей схемы

Это основной механизм для бизнес-валидации, выходящей за рамки стандартных проверок типов.


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

Zod позволяет централизованно изменять сообщения об ошибках:

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

Параметры:

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

Используется для:

  • локализации
  • унификации сообщений
  • скрытия технических деталей

Интернационализация ошибок

Механизм setErrorMap часто применяется для многоязычных систем:

z.setErrorMap((issue) => {
  const messages = {
    invalid_type: "Неверный тип данных",
    too_small: "Значение слишком маленькое"
  }

  return {
    message: messages[issue.code] ?? "Ошибка"
  }
})

Преобразование ошибок для API

В серверной логике ZodError часто преобразуется в стандартизированный HTTP-ответ:

return {
  success: false,
  errors: error.issues.map(i => ({
    field: i.path.join("."),
    message: i.message
  }))
}

Результат:

{
  "success": false,
  "errors": [
    {
      "field": "user.email",
      "message": "Invalid email"
    }
  ]
}

Такой формат удобен для фронтенда и не зависит от внутренней структуры Zod.


Нормализация ошибок для форм

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

const errors = Object.fromEntries(
  error.issues.map(issue => [
    issue.path.join("."),
    issue.message
  ])
)

Результат:

{
  "user.name": "Required",
  "user.email": "Invalid email"
}

Объединение нескольких ошибок валидации

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

const resultA = schemaA.safeParse(data)
const resultB = schemaB.safeParse(data)

if (!resultA.success || !resultB.success) {
  const combined = [
    ...(resultA.success ? [] : resultA.error.issues),
    ...(resultB.success ? [] : resultB.error.issues)
  ]
}

Это полезно при составных моделях данных или multi-step валидации.


Особенности поведения при раннем выходе

По умолчанию Zod собирает все ошибки, не прерывая проверку после первой. Это поведение обеспечивает:

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

Однако при использовании parse с abortEarly (в некоторых конфигурациях окружения) возможно изменение поведения на “первая ошибка”.


Роль path в маршрутизации ошибок

Поле path является ключевым элементом для интеграции с UI и state-менеджерами. Оно позволяет:

  • привязывать ошибки к конкретным input-полям
  • обновлять состояние формы точечно
  • строить динамические интерфейсы ошибок

Пример маршрутизации:

setFieldError(issue.path.join("."), issue.message)

Обработка неизвестных полей

При использовании .strict() Zod генерирует ошибку unrecognized_keys:

{
  code: "unrecognized_keys",
  keys: ["extraField"]
}

Это важно для систем, где требуется жёсткий контракт данных (API, микросервисы, конфигурации).


Расширенная диагностика через ZodError

ZodError также поддерживает доступ к полному объекту ошибок без преобразований:

  • error.issues
  • error.name
  • error.message
  • error.stack (в runtime)

Это позволяет интегрировать Zod в системы логирования и мониторинга, сохраняя полную трассировку валидации.