Валидация вложенных данных — одна из ключевых задач при работе с API, формами, конфигурациями и сложными объектами состояния. Библиотека Zod предоставляет мощные механизмы описания вложенных схем с полной типизацией и поддержкой глубоких структур.
Вложенные структуры создаются через комбинацию
z.object() внутри других объектов.
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
address: z.object({
city: z.string(),
street: z.string(),
zipCode: z.string(),
}),
});
Проверка:
const result = UserSchema.parse({
id: 1,
name: "Alex",
address: {
city: "Berlin",
street: "Main Street",
zipCode: "10115",
},
});
Тип автоматически выводится:
type User = z.infer<typeof UserSchema>;
Результат:
type User = {
id: number;
name: string;
address: {
city: string;
street: string;
zipCode: string;
};
};
Zod поддерживает неограниченную глубину вложенности.
const CompanySchema = z.object({
name: z.string(),
owner: z.object({
personal: z.object({
firstName: z.string(),
lastName: z.string(),
}),
contacts: z.object({
email: z.string().email(),
phone: z.string(),
}),
}),
});
Использование:
CompanySchema.parse({
name: "Tech Corp",
owner: {
personal: {
firstName: "John",
lastName: "Smith",
},
contacts: {
email: "john@example.com",
phone: "+123456789",
},
},
});
Один из самых распространённых сценариев — массивы объектов внутри объекта.
const OrderSchema = z.object({
orderId: z.string(),
products: z.array(
z.object({
id: z.number(),
title: z.string(),
price: z.number(),
})
),
});
Пример:
OrderSchema.parse({
orderId: "ORD-001",
products: [
{
id: 1,
title: "Laptop",
price: 1200,
},
{
id: 2,
title: "Mouse",
price: 50,
},
],
});
Массивы могут содержать объекты с собственными вложенными массивами.
const BlogSchema = z.object({
title: z.string(),
comments: z.array(
z.object({
text: z.string(),
author: z.object({
id: z.number(),
username: z.string(),
}),
replies: z.array(
z.object({
text: z.string(),
})
),
})
),
});
Отдельные вложенные структуры могут быть необязательными.
const ProfileSchema = z.object({
username: z.string(),
social: z
.object({
twitter: z.string(),
github: z.string(),
})
.optional(),
});
Допустимые данные:
ProfileSchema.parse({
username: "alex",
});
Или:
ProfileSchema.parse({
username: "alex",
social: {
twitter: "@alex",
github: "alexdev",
},
});
nullable() разрешает значение null.
const ArticleSchema = z.object({
title: z.string(),
metadata: z
.object({
views: z.number(),
likes: z.number(),
})
.nullable(),
});
const ConfigSchema = z.object({
cache: z
.object({
enabled: z.boolean(),
})
.nullable()
.optional(),
});
Допустимы:
{}
{
cache: null
}
{
cache: {
enabled: true
}
}
partial() делает поля объекта необязательными.
const SettingsSchema = z.object({
theme: z.string(),
language: z.string(),
});
const PartialSettings = SettingsSchema.partial();
Результат:
{
theme?: string;
language?: string;
}
Стандартный partial() работает только на верхнем
уровне.
const UserSchema = z.object({
profile: z.object({
firstName: z.string(),
lastName: z.string(),
}),
});
const PartialUser = UserSchema.partial();
Результат:
{
profile?: {
firstName: string;
lastName: string;
};
}
Внутренние поля остаются обязательными.
Для глубокой модификации применяется deepPartial():
const DeepPartialUser = UserSchema.deepPartial();
Теперь:
{
profile?: {
firstName?: string;
lastName?: string;
};
}
Метод extend() позволяет дополнять схемы.
const AddressSchema = z.object({
city: z.string(),
street: z.string(),
});
const ExtendedAddressSchema = AddressSchema.extend({
country: z.string(),
});
Одна из главных сильных сторон Zod — композиция.
const AddressSchema = z.object({
city: z.string(),
street: z.string(),
});
const UserSchema = z.object({
name: z.string(),
address: AddressSchema,
});
const CompanySchema = z.object({
title: z.string(),
office: AddressSchema,
});
const BaseSchema = z.object({
id: z.number(),
});
const ProfileSchema = z.object({
username: z.string(),
});
const UserSchema = BaseSchema.merge(ProfileSchema);
Результат:
{
id: number;
username: string;
}
Извлечение части схемы.
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
});
const PublicUserSchema = UserSchema.pick({
id: true,
name: true,
});
const PrivateUserSchema = UserSchema.omit({
password: true,
});
.elementПолучение схемы элемента массива:
const TagsSchema = z.array(z.string());
const TagElement = TagsSchema.element;
record() полезен для динамических ключей.
const TranslationSchema = z.object({
translations: z.record(z.string()),
});
Пример:
{
translations: {
en: "Hello",
de: "Hallo",
fr: "Bonjour",
}
}
const PaymentSchema = z.object({
method: z.union([
z.object({
type: z.literal("card"),
cardNumber: z.string(),
}),
z.object({
type: z.literal("paypal"),
email: z.string().email(),
}),
]),
});
Более эффективный вариант:
const PaymentSchema = z.object({
payment: z.discriminatedUnion("type", [
z.object({
type: z.literal("card"),
cardNumber: z.string(),
}),
z.object({
type: z.literal("paypal"),
email: z.string(),
}),
]),
});
Рекурсивные структуры требуют z.lazy().
const CategorySchema = z.lazy(() =>
z.object({
name: z.string(),
children: z.array(CategorySchema),
})
);
Использование:
CategorySchema.parse({
name: "Programming",
children: [
{
name: "JavaScript",
children: [],
},
{
name: "Python",
children: [],
},
],
});
const CommentSchema = z.lazy(() =>
z.object({
id: z.number(),
text: z.string(),
replies: z.array(CommentSchema),
})
);
Методы можно применять на любом уровне.
const UserSchema = z.object({
profile: z.object({
email: z.string().email(),
age: z.number().min(18),
}),
});
const PasswordSchema = z.object({
credentials: z.object({
password: z.string(),
confirmPassword: z.string(),
}),
}).refine(
(data) =>
data.credentials.password ===
data.credentials.confirmPassword,
{
path: ["credentials", "confirmPassword"],
message: "Пароли не совпадают",
}
);
const CartSchema = z.object({
items: z.array(
z.object({
price: z.number(),
quantity: z.number(),
})
),
}).superRefine((data, ctx) => {
const total = data.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
if (total <= 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Некорректная сумма заказа",
});
}
});
При ошибке Zod возвращает подробный путь к полю.
const Schema = z.object({
user: z.object({
profile: z.object({
age: z.number(),
}),
}),
});
Некорректные данные:
Schema.parse({
user: {
profile: {
age: "18",
},
},
});
Ошибка:
[
{
code: "invalid_type",
expected: "number",
received: "string",
path: ["user", "profile", "age"],
}
]
const result = Schema.safeParse(data);
if (!result.success) {
console.log(result.error.format());
}
format() особенно полезен для вложенных структур:
{
user: {
profile: {
age: {
_errors: [
"Expected number"
]
}
}
}
}
const UserSchema = z.object({
profile: z.object({
firstName: z.string(),
lastName: z.string(),
}),
}).transform((data) => ({
...data,
profile: {
...data.profile,
fullName:
`${data.profile.firstName} ${data.profile.lastName}`,
},
}));
const Schema = z.object({
settings: z.object({
retries: z.preprocess(
(value) => Number(value),
z.number()
),
}),
});
По умолчанию лишние поля удаляются.
const UserSchema = z.object({
name: z.string(),
});
UserSchema.parse({
name: "Alex",
extra: true,
});
Результат:
{
name: "Alex"
}
Для строгой проверки:
const StrictSchema = z
.object({
name: z.string(),
})
.strict();
Теперь лишние поля вызовут ошибку.
const FlexibleSchema = z
.object({
name: z.string(),
})
.passthrough();
const StripSchema = z
.object({
name: z.string(),
})
.strip();
const CoordinatesSchema = z.tuple([
z.number(),
z.number(),
]);
const LocationSchema = z.object({
coordinates: CoordinatesSchema,
});
const Schema = z.object({
tags: z.set(z.string()),
});
const Schema = z.object({
dictionary: z.map(
z.string(),
z.number()
),
});
const RoleSchema = z.enum([
"admin",
"user",
"guest",
]);
const UserSchema = z.object({
role: RoleSchema,
});
Крупные приложения обычно строятся через независимые схемы.
const GeoSchema = z.object({
lat: z.number(),
lng: z.number(),
});
const AddressSchema = z.object({
city: z.string(),
geo: GeoSchema,
});
const UserSchema = z.object({
name: z.string(),
address: AddressSchema,
});
const CompanySchema = z.object({
title: z.string(),
employees: z.array(UserSchema),
});
Такой подход:
const ApiResponseSchema = z.object({
success: z.boolean(),
data: z.object({
users: z.array(
z.object({
id: z.number(),
profile: z.object({
username: z.string(),
email: z.string().email(),
}),
posts: z.array(
z.object({
id: z.number(),
title: z.string(),
comments: z.array(
z.object({
id: z.number(),
text: z.string(),
})
),
})
),
})
),
}),
meta: z.object({
page: z.number(),
total: z.number(),
}),
});
Такие схемы особенно востребованы: