Обработка промисов

Базовая концепция асинхронной схемы

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

Ключевое отличие заключается в использовании методов:

  • parseAsync
  • safeParseAsync
  • z.promise(...)

Синхронные методы parse и safeParse не поддерживают промисы и завершаются сразу, тогда как асинхронные аналоги позволяют обрабатывать данные после разрешения Promise.


Схема z.promise как обёртка над асинхронным значением

Конструкция z.promise(schema) описывает тип, который является промисом, возвращающим валидируемое значение.

import { z } from "zod";

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
});

const userPromiseSchema = z.promise(userSchema);

Такое описание означает:

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

Поведение parseAsync и safeParseAsync

Асинхронная обработка требует явного использования соответствующих методов:

const result = await userPromiseSchema.parseAsync(fetchUser());

или безопасный вариант:

const result = await userPromiseSchema.safeParseAsync(fetchUser());

Разница:

  • parseAsync → выбрасывает ZodError при неудаче
  • safeParseAsync → возвращает объект { success, data, error }

Валидация результата промиса

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

const schema = z.promise(
  z.object({
    id: z.number(),
  })
);

await schema.parseAsync(Promise.resolve({ id: "not a number" }));

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


Комбинирование с fetch и внешними API

Типичный сценарий — проверка ответа HTTP-запроса:

const apiResponseSchema = z.promise(
  z.object({
    id: z.number(),
    username: z.string(),
  })
);

async function getUser() {
  return fetch("/api/user")
    .then(res => res.json());
}

const user = await apiResponseSchema.parseAsync(getUser());

Здесь происходит цепочка:

  1. fetch возвращает Promise<Response>
  2. .json() возвращает Promise<any>
  3. Zod ожидает завершения промиса
  4. затем проверяет структуру объекта

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

Zod позволяет добавлять асинхронные проверки без оборачивания в z.promise.

const schema = z.object({
  email: z.string().email(),
}).refine(async (data) => {
  const exists = await checkEmailExists(data.email);
  return !exists;
}, {
  message: "Email уже используется",
});

Такая проверка требует использования parseAsync, иначе выполнение завершится некорректно.


superRefine и детальная работа с ошибками

Для более точного контроля используется superRefine, позволяющий добавлять ошибки вручную:

const schema = z.object({
  password: z.string(),
}).superRefine(async (data, ctx) => {
  const isWeak = await checkPasswordStrength(data.password);

  if (isWeak) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Слабый пароль",
      path: ["password"],
    });
  }
});

Механика:

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

Поведение transform в асинхронных схемах

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

const schema = z.object({
  userId: z.string(),
}).transform(async (data) => {
  const user = await fetchUserById(data.userId);
  return {
    ...data,
    user,
  };
});

Важно:

  • трансформация выполняется только при parseAsync
  • возвращаемое значение может быть Promise
  • результат трансформации становится итоговым значением схемы

Пайплайн выполнения асинхронной схемы

При использовании промисов процесс валидации проходит несколько этапов:

  1. Получение Promise
  2. Ожидание его завершения
  3. Проверка структуры данных
  4. Выполнение refine / superRefine
  5. Выполнение transform
  6. Возврат итогового значения

Любая ошибка на любом этапе прерывает цепочку.


Сочетание z.promise и обычных схем

z.promise часто комбинируется с базовыми схемами для строгого описания API:

const postSchema = z.object({
  id: z.number(),
  title: z.string(),
});

const postsSchema = z.promise(z.array(postSchema));

Здесь описывается:

  • асинхронное получение массива
  • проверка каждого элемента массива
  • строгая типизация структуры

Ошибки и ZodError в асинхронной модели

При асинхронной валидации ошибки сохраняют структуру:

try {
  await schema.parseAsync(data);
} catch (err) {
  if (err instanceof z.ZodError) {
    console.log(err.issues);
  }
}

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

  • ошибки могут возникнуть после await
  • стек ошибок включает путь до поля (path)
  • агрегируются все нарушения, а не только первое

Отличия sync и async схем

Характеристика parse parseAsync
Поддержка Promise нет да
async refine нет да
superRefine async нет да
transform async нет да

Асинхронные схемы требуют дисциплины: любая операция, возвращающая Promise, автоматически переводит схему в async-режим.


Типичные архитектурные сценарии

Валидация API ответа

const responseSchema = z.promise(
  z.object({
    data: z.array(z.number()),
  })
);

Проверка бизнес-логики

const schema = z.object({
  username: z.string(),
}).refine(async (data) => {
  return !(await isBanned(data.username));
});

Обогащение данных

const schema = z.object({
  productId: z.string(),
}).transform(async (data) => {
  const product = await loadProduct(data.productId);
  return product;
});

Внутренние ограничения и особенности

Асинхронная модель накладывает ряд ограничений:

  • невозможно использовать parse при наличии async-операций
  • любые await внутри схем требуют parseAsync
  • порядок выполнения строго последовательный
  • каждая стадия ожидает завершения предыдущей

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


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

Сложные структуры часто комбинируют несколько уровней асинхронности:

const commentSchema = z.object({
  text: z.string(),
});

const postSchema = z.object({
  id: z.number(),
  comments: z.promise(z.array(commentSchema)),
});

В этом случае:

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

Использование в цепочках обработки данных

Асинхронные схемы удобно включаются в пайплайны:

const schema = z.promise(
  z.string()
    .transform(async (value) => value.trim())
    .refine(async (value) => value.length > 0)
);

Здесь данные проходят:

  • получение строки
  • асинхронную нормализацию
  • асинхронную проверку

Каждый шаг может зависеть от внешних ресурсов.