Валидация данных в Zod строится на композиции схем, примитивов и
цепочек преобразований. Одним из ключевых механизмов расширения
стандартной синхронной валидации являются асинхронные проверки через
refine, superRefine и соответствующие
асинхронные методы парсинга. Они позволяют подключать внешние источники
данных: базы, HTTP API, файловые системы и любые операции с задержкой
выполнения.
По умолчанию Zod выполняет валидацию синхронно. Это означает, что все проверки должны завершаться немедленно в рамках текущего вызова. Однако в реальных приложениях часто требуется:
Для таких случаев используются асинхронные варианты API:
parseAsyncsafeParseAsyncrefine и superRefineМетод 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>.
Асинхронные проверки не работают через обычный
parse.
schema.parse("test"); // ошибка, если refine асинхронный
await schema.parseAsync("test");
или
const result = await schema.safeParseAsync("test");
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 предоставляет более низкоуровневый контроль
над ошибками. Он позволяет добавлять несколько ошибок и указывать путь
до поля.
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 уже используется"
});
}
});
boolean или
Promise<boolean>;ctx.addIssue;const usernameSchema = z.string().min(3).refine(async (username) => {
const user = await db.users.findUnique({ wh ere: { username } });
return !user;
}, {
message: "Имя пользователя уже занято"
});
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: "Недействительный токен"
});
Любая попытка использовать parse приведёт к ошибке или
некорректному поведению.
Асинхронные проверки могут вызывать:
Пример проблемного случая:
z.array(
z.string().refine(async (id) => {
return await api.check(id);
})
);
Каждый элемент массива создаёт отдельный запрос.
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 из уровня “схемы данных” в уровень “бизнес-валидации”. Это приводит к нескольким архитектурным последствиям:
Практическим подходом является разграничение:
Пример:
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);
});
При увеличении нагрузки применяются подходы:
Асинхронные refine и superRefine расширяют
Zod до уровня интеграции с внешними системами, превращая схемы в
гибридный инструмент валидации и бизнес-логики. Их использование требует
строгого контроля производительности, структуры запросов и разделения
ответственности между слоями приложения.