В крупных JavaScript- и TypeScript-проектах переход на строгую валидацию данных редко выполняется одномоментно. Обычно кодовая база уже содержит:
Постепенное внедрение позволяет:
Наиболее безопасная стратегия — внедрение Zod только в новые модули.
import { z } from "zod"
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
name: z.string()
})
async function loadUser(id: number) {
const response = await fetch(`/api/users/${id}`)
const json = await response.json()
return UserSchema.parse(json)
}
Старый код продолжает работать без изменений, а новые участки получают:
function createUser(data: any) {
if (!data.email) {
throw new Error("Email required")
}
if (typeof data.age !== "number") {
throw new Error("Age invalid")
}
return {
email: data.email,
age: data.age
}
}
Подобные проверки быстро становятся:
import { z } from "zod"
const CreateUserSchema = z.object({
email: z.string().email(),
age: z.number()
})
function createUser(data: unknown) {
const validated = CreateUserSchema.parse(data)
return validated
}
Во время миграции приложение может содержать большое количество
некорректных данных. Использование parse() иногда приводит
к массовым исключениям.
В таких случаях предпочтительнее safeParse().
const result = CreateUserSchema.safeParse(data)
if (!result.success) {
console.error(result.error)
return null
}
return result.data
}
Одна из самых эффективных стратегий внедрения — валидация только внешних источников данных.
app.post("/users", (req, res) => {
const body = UserSchema.parse(req.body)
saveUser(body)
})
const QuerySchema = z.object({
page: z.coerce.number().default(1)
})
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number()
})
const env = EnvSchema.parse(process.env)
const EventSchema = z.object({
type: z.string(),
payload: z.any()
})
В legacy-проектах часто невозможно изменить существующие функции.
В таких случаях используется слой адаптации.
function saveUser(user: any) {
database.insert(user)
}
const UserSchema = z.object({
id: z.number(),
email: z.string().email()
})
function validatedSaveUser(input: unknown) {
const user = UserSchema.parse(input)
return saveUser(user)
}
Такой подход:
Один из наиболее эффективных подходов.
Валидируются:
Схемы начинают использоваться внутри бизнес-логики.
Zod становится единым источником типов.
Во многих проектах уже существуют интерфейсы TypeScript.
interface User {
id: number
email: string
}
Проблема — отсутствие runtime-проверки.
const UserSchema = z.object({
id: z.number(),
email: z.string().email()
})
type User = z.infer<typeof UserSchema>
Теперь:
Полная миграция может занимать месяцы.
Допустимо временное сосуществование.
interface LegacyUser {
id: number
email: string
}
const UserSchema = z.object({
id: z.number(),
email: z.string()
})
На промежуточном этапе:
При работе с backend-архитектурой полезно валидировать DTO.
const CreatePostDto = z.object({
title: z.string().min(3),
content: z.string(),
tags: z.array(z.string())
})
function createPost(input: unknown) {
const dto = CreatePostDto.parse(input)
return service.create(dto)
}
Во многих старых проектах активно используется any.
Zod помогает постепенно переходить к unknown.
function process(data: any) {
return data.user.name
}
function process(data: unknown) {
const parsed = UserSchema.parse(data)
return parsed.name
}
Иногда невозможно сразу описать полную схему объекта.
В таких случаях применяется partial().
const UserSchema = z.object({
id: z.number(),
email: z.string(),
age: z.number()
})
const PartialUserSchema = UserSchema.partial()
Можно валидировать только известные поля:
PartialUserSchema.parse({
email: "admin@example.com"
})
Во время миграции часто важно не блокировать работу приложения.
const result = UserSchema.safeParse(data)
if (!result.success) {
logger.warn(result.error)
return data
}
Подход особенно полезен:
Сначала схема может быть максимально мягкой.
const UserSchema = z.object({
email: z.string()
})
const UserSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
})
Старые API часто возвращают лишние поля.
По умолчанию Zod их удаляет.
const UserSchema = z.object({
id: z.number()
})
UserSchema.parse({
id: 1,
legacyField: true
})
Поле legacyField будет отброшено.
const UserSchema = z.object({
id: z.number()
}).passthrough()
Теперь дополнительные поля сохраняются.
Во время миграции важно контролировать неизвестные свойства.
Удаляет лишние поля.
z.object({
id: z.number()
}).strip()
Сохраняет лишние поля.
z.object({
id: z.number()
}).passthrough()
Вызывает ошибку при наличии лишних полей.
z.object({
id: z.number()
}).strict()
Формы — один из лучших кандидатов для внедрения Zod.
const schema = z.object({
email: z.string().email(),
password: z.string().min(8)
})
useForm({
resolver: zodResolver(schema)
})
На поздних этапах миграции схемы начинают храниться централизованно.
src/
schemas/
user.schema.ts
post.schema.ts
auth.schema.ts
В больших проектах полезно разделять:
schemas/
api/
db/
forms/
internal/
Одна из ключевых стратегий масштабирования.
const BaseUserSchema = z.object({
id: z.number(),
email: z.string().email()
})
const AdminSchema = BaseUserSchema.extend({
role: z.literal("admin")
})
Во время миграции часто нужны разные представления объекта.
const PublicUserSchema = UserSchema.pick({
id: true,
email: true
})
const SafeUserSchema = UserSchema.omit({
password: true
})
Перед масштабной миграцией полезно покрывать схемы тестами.
expect(() => {
UserSchema.parse(validData)
}).not.toThrow()
expect(() => {
UserSchema.parse(invalidData)
}).toThrow()
Zod особенно эффективен как единый контракт между frontend и backend.
export const UserSchema = z.object({
id: z.number(),
email: z.string().email()
})
app.get("/users/:id", () => {
return UserSchema.parse(data)
})
const user = UserSchema.parse(response)
В monorepo-системах миграция обычно выполняется пакетами.
Во время миграции важно учитывать старые форматы данных.
const OldUserSchema = z.object({
name: z.string()
})
const NewUserSchema = z.object({
firstName: z.string(),
lastName: z.string()
})
const CompatibleSchema = z.union([
OldUserSchema,
NewUserSchema
])
При долгосрочной поддержке API полезно хранить версии схем.
schemas/
v1/
v2/
v3/
Zod может не только валидировать, но и преобразовывать данные.
const UserSchema = z.object({
email: z.string()
}).transform(data => ({
...data,
email: data.email.toLowerCase()
}))
Подходит для устаревших форматов.
const NumberSchema = z.preprocess(
value => Number(value),
z.number()
)
Во время постепенной миграции полезно отслеживать:
any;Часто приводит к:
Слишком строгие схемы ломают legacy-данные.
const data: any = UserSchema.parse(input)
Так теряется смысл типизации.
interface User {
id: number
}
const UserSchema = z.object({
id: z.number()
})
Если оба определения поддерживаются вручную, возникает рассинхронизация.
Валидация:
Удаление any.
Создание shared schemas.
Интеграция во все API-контракты.
Переход на schema-first архитектуру.
На позднем этапе Zod становится главным источником структуры данных.
const UserSchema = z.object({
id: z.number(),
email: z.string().email()
})
type User = z.infer<typeof UserSchema>
UserSchema.parse(data)
Схема одновременно описывает: