Асинхронные трансформации

Природа асинхронных преобразований в схемах

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

Zod изначально проектировался как синхронная библиотека валидации схем, однако в экосистеме JavaScript практически любой прикладной сценарий валидации рано или поздно упирается в асинхронные операции. Для этого предусмотрены отдельные механизмы:

  • parseAsync
  • safeParseAsync
  • асинхронные refine
  • асинхронные transform
  • асинхронные superRefine
  • схемы z.promise(...)

Асинхронность в Zod не является «режимом по умолчанию». Она включается явно, через использование соответствующих API.


Базовый контракт асинхронной схемы

Любая схема Zod может быть использована асинхронно, если внутри неё присутствует хотя бы одна асинхронная операция.

Основное правило:

  • parse() — только синхронные схемы
  • parseAsync() — допускает асинхронные трансформации и проверки

При попытке использовать parse() на асинхронной схеме возникает ошибка выполнения.

const schema = z.string().refine(async (val) => {
  return val.startsWith("user_");
});

await schema.parseAsync("user_123");

Асинхронный refine: проверка через внешние источники

Метод refine поддерживает асинхронные функции, что позволяет подключать внешние источники данных.

Пример проверки уникальности пользователя

const userSchema = z.string().refine(async (username) => {
  const exists = await db.users.findUnique({
    where: { username }
  });

  return !exists;
}, {
  message: "Пользователь уже существует"
});

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

  • Zod ожидает Promise
  • false трактуется как ошибка валидации
  • сообщение ошибки задаётся отдельно

superRefine и асинхронная бизнес-валидация

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

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
}).superRefine(async (data, ctx) => {
  const banned = await checkBannedEmailDomain(data.email);

  if (banned) {
    ctx.addIssue({
      path: ["email"],
      message: "Домен запрещён"
    });
  }

  const leaked = await checkPasswordLeak(data.password);

  if (leaked) {
    ctx.addIssue({
      path: ["password"],
      message: "Пароль скомпрометирован"
    });
  }
});

Ключевой момент:

  • ctx.addIssue позволяет накопить несколько ошибок
  • асинхронные операции выполняются последовательно или параллельно (в зависимости от реализации)

Асинхронный transform: преобразование через API

transform позволяет изменять входные данные после валидации. При использовании async возвращается Promise результата.

const schema = z.string().email().transform(async (email) => {
  const user = await fetchUserProfile(email);

  return {
    email,
    name: user.name,
    avatar: user.avatarUrl
  };
});

Результат:

  • вход: строка email
  • выход: объект профиля пользователя

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

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

Pipeline: последовательные асинхронные преобразования

Асинхронные трансформации часто комбинируются в цепочки.

const schema = z.string()
  .transform(async (val) => val.trim())
  .transform(async (val) => val.toLowerCase())
  .transform(async (val) => {
    const normalized = await normalizeUsername(val);
    return normalized;
  });

Каждый этап:

  1. получает результат предыдущего
  2. может возвращать Promise
  3. ожидается Zod-ом автоматически в parseAsync

safeParseAsync: обработка без исключений

Асинхронная версия безопасного парсинга возвращает структуру результата без выбрасывания ошибок.

const result = await schema.safeParseAsync(input);

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

Структура:

  • success: true → данные валидны
  • success: false → массив ошибок

Это основной механизм интеграции с UI и API слоями.


z.promise: схемы для промисов

Zod поддерживает прямую работу с Promise-значениями.

const schema = z.promise(
  z.string().transform(async (val) => {
    return val.toUpperCase();
  })
);

const result = await schema.parseAsync(Promise.resolve("hello"));

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

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

Порядок выполнения асинхронных стадий

Асинхронная обработка в Zod проходит несколько фаз:

  1. Preprocessing

    • синхронная нормализация входа (z.preprocess)
  2. Validation

    • базовые типы и ограничения
  3. Async refine

    • внешние проверки
  4. Async transform

    • преобразование результата
  5. Final output

Важно:

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

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

Хотя preprocess обычно синхронный, он может возвращать Promise через parseAsync.

const schema = z.preprocess(async (val) => {
  const cleaned = await sanitize(val);
  return cleaned;
}, z.string());

Это позволяет:

  • очищать входные данные до валидации
  • интегрировать внешние сервисы нормализации

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

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

  • последовательные transform/refine → выполняются цепочкой
  • независимые проверки внутри superRefine → могут быть параллелизованы вручную

Оптимизация часто выглядит так:

superRefine(async (data, ctx) => {
  const [a, b] = await Promise.all([
    checkA(data),
    checkB(data)
  ]);

  if (!a) ctx.addIssue({ path: ["a"], message: "Ошибка A" });
  if (!b) ctx.addIssue({ path: ["b"], message: "Ошибка B" });
});

Типизация асинхронных трансформаций

TypeScript корректно выводит тип результата даже при использовании async transform.

const schema = z.string().transform(async (val) => {
  return { value: val, length: val.length };
});

type Result = z.infer<typeof schema>;
// Promise<{ value: string; length: number }>

Важно:

  • результат всегда оборачивается в Promise
  • конечный тип требует Awaited<> для извлечения

Ошибки в асинхронных цепочках

Асинхронные ошибки могут возникать в двух формах:

  1. возврат false в refine
  2. выброс исключения внутри async функции
.refine(async (val) => {
  throw new Error("внешний сервис недоступен");
});

Поведение:

  • исключение превращается в ошибку Zod
  • сообщение может быть обобщено

Комбинация async refine и transform

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

const schema = z.string()
  .refine(async (val) => await existsInDb(val))
  .transform(async (val) => {
    return await loadFullEntity(val);
  });

Логика:

  • сначала проверка существования
  • затем загрузка полной модели

Ограничения асинхронной модели

Асинхронные трансформации в Zod имеют архитектурные ограничения:

  • невозможность использования parse() для async схем
  • невозможность частичного await внутри sync pipeline
  • потенциальные задержки при глубокой цепочке трансформаций
  • отсутствие встроенного конкурентного планировщика

По этой причине сложные сценарии часто выносятся в слой сервисов, а Zod используется как слой валидации и нормализации данных.


Итоговая модель поведения

Асинхронные трансформации в Zod формируют модель, где схема становится не только валидатором, но и оркестратором данных:

  • проверка структуры
  • обогащение данных через внешние источники
  • преобразование типов
  • накопление ошибок

Zod в этом контексте выступает как декларативный слой обработки данных, объединяющий синхронную строгость и асинхронную гибкость JavaScript-экосистемы.