Что такое Zod и зачем он нужен

Zod — библиотека для описания и проверки структур данных во время выполнения, ориентированная на строгую типизацию и тесную интеграцию с TypeScript. Основная идея заключается в том, чтобы схема данных существовала одновременно как механизм валидации и как источник типов, автоматически выводимых для статической проверки.

JavaScript изначально не предоставляет встроенного механизма строгой проверки типов данных во время выполнения. Даже при использовании TypeScript типы существуют только на этапе компиляции и исчезают после трансляции кода. Это создаёт разрыв между тем, что ожидает программа, и тем, что реально приходит в рантайме, особенно в следующих случаях:

  • данные из HTTP-запросов
  • ответы внешних API
  • данные из localStorage или cookies
  • переменные окружения
  • ввод пользователя в формах

Любой из этих источников может содержать неожиданные структуры. Отсутствие проверки приводит к ошибкам вида undefined is not a function, cannot read property of undefined и аналогичным сбоям.

Суть подхода Zod

Zod вводит концепцию схемы как единого источника правды:

  • схема описывает структуру данных
  • схема проверяет данные в рантайме
  • схема автоматически генерирует TypeScript-тип

Таким образом устраняется дублирование логики: не нужно отдельно писать интерфейсы и отдельные валидаторы.

Простейшая схема:

import { z } from "zod";

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

Проверка данных

Основной механизм работы — парсинг входных данных через схему.

Строгая проверка

UserSchema.parse({
  id: 1,
  name: "Alex",
});

Если данные не соответствуют схеме, выбрасывается исключение.

Безопасная проверка

const result = UserSchema.safeParse(input);

Возвращается объект вида:

  • success: true и данные при успехе
  • success: false и список ошибок при неудаче

Такой подход предпочтителен для API и пользовательского ввода, где падение приложения недопустимо.

Вывод типов

Одно из ключевых преимуществ — автоматическое получение TypeScript-типа:

type User = z.infer<typeof UserSchema>;

В результате тип User полностью соответствует схеме. Это устраняет проблему рассинхронизации между типами и реальной валидацией.

Базовые примитивы

Библиотека предоставляет набор стандартных валидаторов:

  • z.string()
  • z.number()
  • z.boolean()
  • z.date()
  • z.bigint()

Каждый примитив уже включает базовую проверку.

Пример:

const Age = z.number().min(0).max(120);

Составные структуры

Объекты

const Product = z.object({
  title: z.string(),
  price: z.number(),
});

Объекты можно вкладывать друг в друга, формируя сложные структуры.

Массивы

const Tags = z.array(z.string());

Объединения

const Id = z.union([z.string(), z.number()]);

Union позволяет описывать несколько допустимых форм данных.

Опциональные и nullable поля

const Schema = z.object({
  description: z.string().optional(),
  comment: z.string().nullable(),
});
  • optional() — поле может отсутствовать
  • nullable() — поле может быть null

Эти различия важны при работе с API, где отсутствие значения и явный null имеют разную семантику.

Преобразование данных

Zod поддерживает трансформации входных значений:

const NumberFromString = z.string().transform((val) => Number(val));

Это позволяет не только проверять данные, но и нормализовать их в нужный формат.

Валидация с уточнениями

Можно добавлять кастомные проверки:

const Password = z.string().refine((val) => val.length >= 8, {
  message: "Слишком короткий пароль",
});

Это расширяет базовые ограничения и позволяет реализовать бизнес-логику прямо в схеме.

Работа с объектами сложной структуры

Схемы поддерживают композицию:

const Address = z.object({
  city: z.string(),
  zip: z.string(),
});

const User = z.object({
  name: z.string(),
  address: Address,
});

Такой подход позволяет строить переиспользуемые блоки схем.

Парсинг неизвестных данных

Типичный сценарий — работа с JSON:

const data = JSON.parse(raw);
const validated = UserSchema.parse(data);

Если структура нарушена, ошибка возникает на этапе парсинга, а не глубже в бизнес-логике.

Отличия от других библиотек

По сравнению с аналогами:

  • Joi — более старый подход, не ориентирован на TypeScript-инференс
  • Yup — слабее интеграция с типами и менее строгая модель
  • Zod — полностью TypeScript-first, минимизирует дублирование типов

Ключевое отличие заключается в том, что схема одновременно является валидатором и источником типов, а не двумя разными сущностями.

Использование в API-слое

При обработке HTTP-запросов схема выступает как фильтр входных данных:

app.post("/user", (req, res) => {
  const result = UserSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json(result.error.format());
  }

  const user = result.data;
});

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

Конфигурационные данные

Переменные окружения часто приходят как строки, даже если ожидаются числа или булевы значения:

const EnvSchema = z.object({
  PORT: z.string().transform(Number),
  DEBUG: z.string().transform((v) => v === "true"),
});

Это делает конфигурацию предсказуемой и безопасной.

Ограничения и особенности модели

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

  • уменьшением runtime-ошибок
  • улучшенной читаемостью контрактов данных
  • строгой проверкой входных данных

Композиция схем

Схемы можно объединять:

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

const ExtendedUser = BaseUser.extend({
  role: z.string(),
});

Это позволяет строить иерархии моделей без дублирования описаний.

Динамические схемы

Поддерживается создание схем на основе параметров:

const createSchema = (minAge) =>
  z.object({
    age: z.number().min(minAge),
  });

Такой подход используется в случаях, когда правила зависят от контекста выполнения.

Ошибки и диагностика

Ошибки валидации содержат структурированную информацию:

  • путь до поля
  • тип ошибки
  • ожидаемое значение
  • фактическое значение

Это упрощает отладку и построение пользовательских сообщений об ошибках.

Роль в архитектуре приложений

Zod часто используется как слой границы между внешним миром и внутренней логикой приложения. Он выполняет функцию контракта данных:

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

Такой подход особенно важен в распределённых системах и при работе с микросервисами, где данные проходят через множество независимых компонентов.