Асинхронные refine проверки

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

Асинхронная модель в Zod

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

  • проверка уникальности значения в базе данных;
  • валидация токена через внешний сервис;
  • проверка существования ресурса по API;
  • бизнес-правила, зависящие от удалённых данных.

Для таких случаев используются асинхронные варианты API:

  • parseAsync
  • safeParseAsync
  • асинхронные refine и superRefine

Асинхронный refine

Метод refine в Zod может возвращать Promise<boolean>, что переводит проверку в асинхронный режим.

Базовый синтаксис

import { z } fr om "zod";

const schema = z.string().refine(async (value) => {
  const res = await fetch(`https://api.example.com/check?value=${value}`);
  const data = await res.json();
  return data.valid === true;
}, {
  message: "Значение не прошло удалённую проверку"
});

Ключевой момент заключается в том, что функция проверки становится асинхронной, а результатом может быть Promise<boolean>.


Требование использования parseAsync

Асинхронные проверки не работают через обычный parse.

Неправильное использование

schema.parse("test"); // ошибка, если refine асинхронный

Корректный вариант

await schema.parseAsync("test");

или

const result = await schema.safeParseAsync("test");

safeParseAsync предпочтителен в сценариях, где требуется обработка ошибок без исключений.


Поведение safeParseAsync

safeParseAsync возвращает структуру:

{
  success: boolean,
  data?: T,
  error?: ZodError
}

Пример:

const result = await schema.safeParseAsync("value");

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

Асинхронные refine полностью интегрируются в эту модель: ошибки агрегируются в ZodError, как и при синхронной валидации.


superRefine и асинхронная логика

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

Синтаксис с async

const schema = z.object({
  email: z.string().email()
}).superRefine(async (data, ctx) => {
  const res = await fetch(`https://api.example.com/users?email=${data.email}`);
  const exists = await res.json();

  if (exists) {
    ctx.addIssue({
      path: ["email"],
      code: "custom",
      message: "Email уже используется"
    });
  }
});

Особенности

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

Отличие refine и superRefine в асинхронном контексте

refine

  • возвращает boolean или Promise<boolean>;
  • одна ошибка на проверку;
  • простой синтаксис;
  • ограниченный контроль над сообщениями.

superRefine

  • не возвращает boolean;
  • ошибки добавляются вручную через ctx.addIssue;
  • поддерживает множественные ошибки;
  • более гибкий для бизнес-логики.

Типичные сценарии применения асинхронных refine

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

const usernameSchema = z.string().min(3).refine(async (username) => {
  const user = await db.users.findUnique({ wh ere: { username } });
  return !user;
}, {
  message: "Имя пользователя уже занято"
});

Проверка через внешний API

const addressSchema = z.string().refine(async (address) => {
  const res = await fetch("https://geo.api/validate", {
    method: "POST",
    body: JSON.stringify({ address })
  });

  const data = await res.json();
  return data.valid;
});

Проверка токенов

const tokenSchema = z.string().refine(async (token) => {
  const res = await authService.verify(token);
  return res.active === true;
}, {
  message: "Недействительный токен"
});

Важные ограничения асинхронных refine

Нельзя использовать в sync parse

Любая попытка использовать parse приведёт к ошибке или некорректному поведению.


Производительность

Асинхронные проверки могут вызывать:

  • блокирующие сетевые запросы;
  • множественные обращения к API при валидации массивов;
  • задержки в цепочках схем.

Пример проблемного случая:

z.array(
  z.string().refine(async (id) => {
    return await api.check(id);
  })
);

Каждый элемент массива создаёт отдельный запрос.


Отсутствие параллельной оптимизации внутри Zod

Zod не агрегирует async-запросы автоматически. Это означает, что:

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

Кэширование асинхронных проверок

Для уменьшения нагрузки используется мемоизация:

const cache = new Map();

const schema = z.string().refine(async (value) => {
  if (cache.has(value)) return cache.get(value);

  const result = await api.check(value);
  cache.set(value, result);

  return result;
});

Обработка ошибок и диагностика

Асинхронные ошибки проходят через стандартный механизм ZodError.

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

{
  issues: [
    {
      code: "custom",
      message: "Ошибка проверки",
      path: []
    }
  ]
}

При использовании superRefine можно точно указывать путь:

ctx.addIssue({
  path: ["profile", "email"],
  message: "Email недоступен"
});

Композиция асинхронных схем

Асинхронные refine можно комбинировать:

const schema = z.string()
  .min(3)
  .refine(async (v) => await checkLength(v))
  .refine(async (v) => await checkBannedWords(v));

Каждая проверка выполняется последовательно, что важно учитывать при проектировании.


Влияние на архитектуру валидации

Асинхронные проверки фактически переводят Zod из уровня “схемы данных” в уровень “бизнес-валидации”. Это приводит к нескольким архитектурным последствиям:

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

Разделение синхронной и асинхронной логики

Практическим подходом является разграничение:

  • синхронные схемы — структура и формат данных;
  • асинхронные refine — внешние проверки.

Пример:

const baseSchema = z.object({
  email: z.string().email(),
  age: z.number().min(18)
});

const asyncSchema = baseSchema.refine(async (data) => {
  return await externalPolicyCheck(data.email, data.age);
});

Поведение в цепочках трансформаций

Асинхронные refine выполняются после transform:

const schema = z.string()
  .transform(v => v.trim())
  .refine(async (v) => await check(v));

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


Особенности отладки

При работе с асинхронными refine часто возникают сложности:

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

Для диагностики используется логирование внутри функции:

.refine(async (v) => {
  console.log("checking:", v);
  return await check(v);
});

Масштабирование асинхронной валидации

При увеличении нагрузки применяются подходы:

  • батчинг запросов вне Zod;
  • предварительная загрузка данных;
  • перенос части проверок в сервисный слой;
  • ограничение числа async refine в одной схеме.

Итоговая роль асинхронных refine

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