Контракт ответа API и необходимость валидации
Ответы внешних API нельзя считать гарантированно корректными даже при
наличии документации и типизации на стороне клиента. Изменение структуры
данных, частичное отсутствие полей, неожиданные null,
расхождение версий — типичные ситуации, приводящие к ошибкам исполнения
в приложении. Типизация TypeScript решает задачу только на этапе
компиляции, тогда как данные API поступают в приложение уже в виде
«сырого» JSON.
Библиотека Zod вводит слой рантайм-валидации, позволяющий проверять соответствие структуры данных заранее определённой схеме. Основная идея заключается в том, что схема становится единым источником истины: она описывает не только тип, но и правила допустимых значений.
Схема как контракт данных
Базовый подход при работе с API заключается в описании структуры ответа через схему:
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
Такая схема задаёт строгий контракт: объект обязан содержать числовой
id, строковый name и строковый
email, соответствующий формату электронной почты.
Проверка выполняется явно:
const result = UserSchema.safeParse(apiResponse);
if (!result.success) {
console.log(result.error.format());
} else {
const user = result.data;
}
Метод safeParse обеспечивает безопасную обработку:
вместо выбрасывания исключения возвращается структурированный результат
с данными или ошибкой.
Структура ответа API и нормализация данных
В реальных системах API часто возвращают данные с дополнительными уровнями вложенности:
{
"status": "ok",
"data": {
"user": {
"id": 1,
"name": "Alex",
"email": "alex@mail.com"
}
}
}
Для подобных случаев используется композиция схем:
const ApiResponseSchema = z.object({
status: z.literal("ok"),
data: z.object({
user: UserSchema,
}),
});
Такой подход позволяет проверять весь «пакет» ответа, а не только его часть. Это снижает вероятность обработки неконсистентных данных.
Обработка необязательных полей
API часто возвращают поля, которые могут отсутствовать или быть
null. В Zod это выражается через optional и
nullable.
const UserSchema = z.object({
id: z.number(),
name: z.string(),
avatarUrl: z.string().url().optional(),
bio: z.string().nullable(),
});
optional() означает отсутствие поля допустимоnullable() допускает значение nullКомбинирование этих модификаторов позволяет точно отражать реальное поведение API без избыточных предположений.
Дефолтные значения и нормализация входных данных
Некоторые API не гарантируют наличие всех полей, но приложение
требует их наличия. В этом случае применяется default:
const SettingsSchema = z.object({
theme: z.string().default("light"),
notifications: z.boolean().default(true),
});
При отсутствии поля в данных оно будет автоматически заполнено указанным значением. Это позволяет приводить внешние данные к стабильной внутренней модели.
Преобразование данных при валидации
Zod поддерживает трансформации, позволяющие не только проверять, но и изменять структуру данных.
const UserSchema = z.object({
id: z.string(),
createdAt: z.string(),
}).transform((data) => ({
id: Number(data.id),
createdAt: new Date(data.createdAt),
}));
После валидации результат уже соответствует внутреннему формату приложения. Это снижает необходимость в дополнительной обработке после получения данных.
Работа с массивами и вложенными структурами
API часто возвращают списки сущностей:
const UsersSchema = z.array(UserSchema);
При более сложных структурах:
const ResponseSchema = z.object({
users: z.array(
z.object({
id: z.number(),
profile: z.object({
name: z.string(),
age: z.number().int(),
}),
})
),
});
Вложенные схемы позволяют описывать сложные графы данных без потери читаемости.
Дискриминированные объединения (discriminated unions)
Один из наиболее мощных инструментов Zod при работе с API — обработка ответов с разными типами данных в зависимости от статуса.
Пример ответа:
{ "status": "success", "data": { "id": 1 } }
{ "status": "error", "message": "Not found" }
Схема:
const SuccessSchema = z.object({
status: z.literal("success"),
data: z.object({
id: z.number(),
}),
});
const ErrorSchema = z.object({
status: z.literal("error"),
message: z.string(),
});
const ApiSchema = z.discriminatedUnion("status", [
SuccessSchema,
ErrorSchema,
]);
Дискриминатор status позволяет автоматически определять
ветку схемы и корректно типизировать результат.
Защита от неизвестных полей
API может возвращать дополнительные поля, не описанные в контракте. По умолчанию Zod их сохраняет, но часто требуется строгая модель.
const StrictUserSchema = z.object({
id: z.number(),
name: z.string(),
}).strict();
Метод strict() приводит к ошибке при наличии неизвестных
ключей. Это важно для контроля изменений API.
Альтернативой является strip():
const UserSchema = z.object({
id: z.number(),
name: z.string(),
}).strip();
В этом случае лишние поля удаляются автоматически.
Проблема частичной валидации
В некоторых сценариях необходимо валидировать только часть ответа.
Для этого используется partial():
const PartialUserSchema = UserSchema.partial();
Все поля становятся необязательными, что удобно при патч-операциях или неполных ответах API.
Переиспользование схем и композиция
Схемы Zod можно и нужно переиспользовать:
const BaseUserSchema = z.object({
id: z.number(),
name: z.string(),
});
const AdminSchema = BaseUserSchema.extend({
permissions: z.array(z.string()),
});
Метод extend позволяет расширять базовые контракты без
дублирования логики.
Также возможно объединение:
const TimestampSchema = z.object({
createdAt: z.string(),
updatedAt: z.string(),
});
const EntitySchema = BaseUserSchema.merge(TimestampSchema);
Асинхронная валидация и API-запросы
Хотя Zod работает синхронно, его часто используют в связке с асинхронными запросами:
async function fetchUser() {
const res = await fetch("/api/user");
const json = await res.json();
return UserSchema.parse(json);
}
При некорректных данных parse выбрасывает исключение,
что позволяет централизованно обрабатывать ошибки.
Обработка ошибок валидации
Ошибки Zod имеют структурированную форму:
const result = UserSchema.safeParse(data);
if (!result.success) {
result.error.issues.forEach((issue) => {
console.log(issue.path, issue.message);
});
}
Каждая ошибка содержит путь к полю и описание проблемы. Это позволяет строить точные сообщения для логирования или диагностики API.
Интеграция с типами TypeScript
Одно из ключевых преимуществ заключается в автоматическом выводе типов:
type User = z.infer<typeof UserSchema>;
Схема становится источником как runtime-валидации, так и статической типизации. Это устраняет дублирование описания структуры данных.
Кеширование и повторное использование схем при масштабировании
В крупных приложениях схемы становятся частью архитектуры слоя API. Они выносятся в отдельные модули и используются повторно для:
Такая унификация снижает риск рассинхронизации между фронтендом и бэкендом.
Валидация как часть доменной модели
Использование схем для API-ответов постепенно смещается в сторону доменной модели. Данные не рассматриваются как «сырые JSON-объекты», а сразу приводятся к структурированным сущностям.
const ProductSchema = z.object({
id: z.number(),
title: z.string(),
price: z.number().nonnegative(),
}).transform((p) => ({
...p,
isExpensive: p.price > 1000,
}));
Таким образом, схема выполняет роль не только фильтра, но и слоя бизнес-логики на этапе входных данных.