Частые ошибки и их решения

Одной из наиболее распространённых ошибок при работе с Zod является использование parse там, где требуется безопасная обработка ошибок. Метод parse выбрасывает исключение при невалидных данных, что часто приводит к неожиданным падениям приложения.

import { z } from "zod";

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

const data = JSON.parse('{"id":"not-a-number"}');

// Ошибка выбросится и прервёт выполнение
schema.parse(data);

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

Альтернативный подход:

const result = schema.safeParse(data);

if (!result.success) {
  console.log(result.error.format());
} else {
  console.log(result.data);
}

Использование safeParse позволяет централизованно контролировать поток ошибок без прерывания выполнения программы.


Ошибки при работе с optional, nullable и default

Частая ошибка заключается в предположении, что эти модификаторы ведут себя одинаково. На практике их семантика существенно различается.

const schema = z.object({
  a: z.string().optional(),
  b: z.string().nullable(),
  c: z.string().default("hello"),
});

Поведение:

  • optional() — поле может отсутствовать полностью
  • nullable() — поле может быть null, но не отсутствовать
  • default() — подставляет значение только при undefined

Типичная ошибка возникает при комбинации:

z.string().optional().default("x");

Фактически default не сработает, если значение undefined уже интерпретировано как “отсутствует поле” в определённых контекстах (например, при частичных объектах или трансформациях).


Некорректное использование transform

transform часто используется как способ “исправить” данные, но приводит к скрытым ошибкам, когда типы перестают соответствовать реальности.

const schema = z.string().transform((val) => val.length);

После трансформации тип становится number, но исходная схема остаётся строковой.

Ошибка возникает при повторном использовании схемы:

type A = z.infer<typeof schema>; // string, но фактически number после transform

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

const base = z.string();
const transformed = base.transform((val) => val.length);

Проблемы с z.infer и TypeScript

Одно из частых заблуждений — ожидание полной идентичности runtime и compile-time типов.

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

type User = z.infer<typeof schema>;

Проблема возникает при использовании refine, transform, superRefine, где TypeScript не всегда корректно отражает изменённую структуру.

const schema = z.string().transform((v) => Number(v));

type T = z.infer<typeof schema>; // number

Но при сложных композициях тип может становиться неточным или слишком широким (unknown).


Ошибки при использовании refine и superRefine

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

z.number().refine((val) => val > 10, {
  message: "Too small",
});

Проблема возникает, когда требуется доступ к нескольким полям — разработчики продолжают использовать refine вместо superRefine.

z.object({
  password: z.string(),
  confirm: z.string(),
}).superRefine((data, ctx) => {
  if (data.password !== data.confirm) {
    ctx.addIssue({
      path: ["confirm"],
      message: "Passwords do not match",
      code: z.ZodIssueCode.custom,
    });
  }
});

Ошибка заключается в попытке реализовать подобную логику через refine, что ограничивает доступ к контексту.


Неправильная работа с union и discriminatedUnion

Обычные union часто вызывают проблемы при парсинге неоднозначных структур.

const schema = z.union([
  z.object({ type: z.literal("a"), a: z.string() }),
  z.object({ type: z.literal("b"), b: z.number() }),
]);

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

Рекомендуемая практика — использование discriminatedUnion:

const schema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("a"), a: z.string() }),
  z.object({ type: z.literal("b"), b: z.number() }),
]);

Ошибка часто возникает при попытке добавить дополнительные варианты без обновления дискриминатора, что ломает проверку.


Ошибки при работе с strict, strip и passthrough

Многие разработчики ожидают, что Zod всегда удаляет лишние поля автоматически.

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

Ошибка возникает при попытке передать дополнительные поля:

schema.parse({ id: 1, extra: true }); // ошибка

При этом:

  • strip (по умолчанию) удаляет лишние поля
  • strict выбрасывает ошибку
  • passthrough сохраняет лишние поля

Частая ошибка — использование strict в API-слоях без осознания последствий, что приводит к неожиданным падениям запросов.


Проблемы с вложенными объектами и deepPartial

partial() работает только на первом уровне, что часто вызывает неправильные ожидания.

const schema = z.object({
  user: z.object({
    name: z.string(),
  }),
}).partial();

Ошибка: name внутри user остаётся обязательным.

Для корректного поведения требуется:

schema.deepPartial();

Игнорирование этого приводит к ошибкам при обработке частичных обновлений (PATCH-запросов).


Ошибки при использовании z.lazy и рекурсивных схем

Рекурсивные структуры часто реализуются неправильно из-за отсутствия lazy.

const schema = z.object({
  name: z.string(),
  child: schema, // ошибка
});

Корректный вариант:

const schema = z.object({
  name: z.string(),
  child: z.lazy(() => schema),
});

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


Ошибки при работе с массивами

Частая ошибка — ожидание, что .array() автоматически валидирует содержимое глубоко и строго.

z.array(z.number()).parse([1, "2", 3]);

Ошибка возникает в интерпретации: "2" не приводится автоматически к числу.

Для приведения типов требуется coerce:

z.array(z.coerce.number());

Ошибки при использовании coerce

coerce часто применяется без понимания побочных эффектов.

z.coerce.number().parse("abc");

Результат: NaN, который проходит валидацию как число в некоторых сценариях.

Это приводит к скрытым багам, особенно при работе с API и формами.


Проблемы с environment variables схемами

Типичная ошибка при валидации переменных окружения:

const schema = z.object({
  PORT: z.number(),
});

process.env.PORT всегда строка, что приводит к постоянным ошибкам.

Корректный подход:

const schema = z.object({
  PORT: z.coerce.number(),
});

Игнорирование этого приводит к нестабильной конфигурации приложения.


Ошибки при повторном использовании схем

Схемы Zod являются иммутабельными, но их композиция может приводить к неожиданным эффектам при реиспользовании.

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

const extended = base.extend({
  name: z.string(),
});

Ошибка возникает, когда предполагается, что base изменяется — на практике создаётся новая схема.


Проблемы с форматированием ошибок

Стандартный error.message часто используется неправильно:

const result = schema.safeParse(data);
console.log(result.error.message);

Проблема: сообщение агрегированное и теряет структуру.

Правильный доступ:

console.log(result.error.format());

или

console.log(result.error.issues);

Игнорирование структуры ошибок приводит к невозможности корректного отображения проблем в UI.


Ошибки при использовании async схем

Zod поддерживает асинхронную валидацию через refine, но частая ошибка — смешивание sync и async API.

await schema.parseAsync(data);

Ошибка возникает при использовании parse вместо parseAsync, что приводит к игнорированию async-валидаций.


Ошибки при чрезмерной вложенности схем

Глубокая композиция схем приводит к ухудшению читаемости и деградации производительности.

z.object({
  a: z.object({
    b: z.object({
      c: z.object({
        d: z.string(),
      }),
    }),
  }),
});

Типичная ошибка — отсутствие декомпозиции и повторное использование вложенных структур, что усложняет сопровождение и тестирование.


Ошибки при использовании brand

brand часто применяется для создания номинальных типов, но забывается при последующих трансформациях.

const UserId = z.string().brand<"UserId">();

После transform бренд может теряться, что ломает типовую безопасность на уровне компиляции.


Ошибки при сочетании preprocess и схем

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

const schema = z.preprocess((val) => Number(val), z.number());

Ошибка возникает, когда val уже является числом или null, что приводит к неожиданным преобразованиям.