Условная валидация с refine и superRefine

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


Базовая идея refine

Метод refine позволяет добавить пользовательскую проверку к уже определённой схеме. Он применяется, когда структура данных корректна на уровне типов, но требуется дополнительная логика валидации.

Общая форма

schema.refine((value) => boolean, {
  message: string
})

Функция получает полностью распарсенное значение и должна вернуть true или false.


Простая условная проверка

import { z } from "zod";

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

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


Ограничения refine

Несмотря на удобство, refine имеет ряд ограничений:

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

Эти ограничения становятся критичными при сложной валидации форм и бизнес-логики.


superRefine как расширенная модель валидации

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

Сигнатура

schema.superRefine((value, ctx) => {
  // логика проверки
})

ctx — объект контекста, содержащий метод addIssue.


Добавление нескольких ошибок

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

  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      code: "custom",
      message: "Пароли должны совпадать",
      path: ["confirmPassword"]
    });
  }
});

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


Структура ctx.addIssue

Метод принимает объект с полями:

  • code — тип ошибки (обычно "custom");
  • message — текст ошибки;
  • path — путь к полю, к которому относится ошибка;
  • дополнительные поля для расширенной диагностики.

Условная логика валидации

superRefine позволяет реализовывать зависимые условия между полями.

Пример зависимой валидации

const schema = z.object({
  type: z.enum(["email", "phone"]),
  value: z.string()
}).superRefine((data, ctx) => {
  if (data.type === "email" && !data.value.includes("@")) {
    ctx.addIssue({
      code: "custom",
      message: "Некорректный email",
      path: ["value"]
    });
  }

  if (data.type === "phone" && data.value.length < 10) {
    ctx.addIssue({
      code: "custom",
      message: "Некорректный номер телефона",
      path: ["value"]
    });
  }
});

Логика становится полностью динамической и зависит от состояния всего объекта.


Валидация массивов с superRefine

Особенно полезен метод при проверке коллекций, где элементы зависят друг от друга.

const schema = z.array(z.number()).superRefine((arr, ctx) => {
  if (arr.length > 0 && arr[0] !== 0) {
    ctx.addIssue({
      code: "custom",
      message: "Первый элемент должен быть 0",
      path: [0]
    });
  }

  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < arr[i - 1]) {
      ctx.addIssue({
        code: "custom",
        message: "Массив должен быть неубывающим",
        path: [i]
      });
    }
  }
});

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


Различие refine и superRefine

refine

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

superRefine

  • множественные ошибки;
  • точное позиционирование ошибок;
  • доступ к контексту валидации;
  • поддержка сложной бизнес-логики.

Комбинирование с базовыми валидаторами

Часто refine и superRefine используются вместе с базовыми схемами:

const schema = z.object({
  username: z.string().min(3),
  age: z.number().int()
}).superRefine((data, ctx) => {
  if (data.age < 18 && data.username === "admin") {
    ctx.addIssue({
      code: "custom",
      message: "Администратор должен быть совершеннолетним",
      path: ["age"]
    });
  }
});

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


Контроль бизнес-правил через superRefine

В сложных системах валидация часто отражает бизнес-ограничения, а не только типы данных.

const orderSchema = z.object({
  items: z.array(z.object({
    price: z.number(),
    quantity: z.number()
  })),
  discountCode: z.string().optional()
}).superRefine((order, ctx) => {
  const total = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);

  if (order.discountCode && total < 100) {
    ctx.addIssue({
      code: "custom",
      message: "Скидка доступна только при сумме заказа от 100",
      path: ["discountCode"]
    });
  }
});

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


Производительность и архитектурные особенности

Использование superRefine влияет на архитектуру схем:

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

Поэтому сложные проверки обычно группируются и минимизируются по количеству операций.


Типизация результата

Одним из ключевых преимуществ Zod остаётся строгая типизация. Ни refine, ни superRefine не изменяют тип результата — они влияют только на валидность данных.

const schema = z.string().refine((val) => val.length > 3);

type Result = z.infer<typeof schema>; // string

Тип остаётся неизменным независимо от добавленной логики.


Использование в реальных формах

При обработке форм часто требуется комбинировать несколько уровней валидации:

  • структурная проверка через object и примитивные схемы;
  • локальные ограничения через min, max, regex;
  • межполевая логика через refine;
  • сложные сценарии через superRefine.

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


Ошибки и их агрегация

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

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

Это делает возможной интеграцию с UI-формами, где требуется отображение всех ошибок одновременно.