Валидация данных базы данных

Данные, возвращаемые из базы данных, не обладают гарантиями корректности на уровне приложения. Даже при строгой схеме таблиц типы в SQL и типы в JavaScript не совпадают напрямую: строки могут приходить вместо чисел, NULL вместо объектов, даты — в виде строк, а вложенные структуры часто требуют ручной нормализации. В таких условиях необходим слой валидации, который приводит «сырые» данные к предсказуемой форме и одновременно отсеивает некорректные записи.

Библиотека Zod решает задачу проверки и преобразования данных на уровне выполнения программы, формируя строгие схемы, которые одновременно описывают структуру данных и выполняют их валидацию.


Принцип работы Zod при обработке данных из БД

Zod строится вокруг идеи схемы как исполняемого описания данных. Схема не только описывает тип, но и:

  • проверяет соответствие структуры;
  • преобразует входные значения;
  • возвращает типизированный результат;
  • генерирует детализированные ошибки.

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


Базовая модель валидации строк из таблиц

Рассмотрим типичный случай: таблица пользователей.

type RawUserRow = {
  id: unknown;
  email: unknown;
  age: unknown;
  created_at: unknown;
};

Данные приходят из SQL-драйвера без строгих типов. С помощью Zod определяется схема:

import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  email: z.string().email(),
  age: z.number().int().positive(),
  created_at: z.string().datetime()
});

Применение:

const parsedUser = UserSchema.parse(rowFromDb);

При несоответствии структуры выбрасывается исключение, содержащее описание ошибок по каждому полю.


Работа с несовпадением типов SQL и JavaScript

База данных часто возвращает значения в формате, отличном от ожидаемого:

  • INTEGER → иногда приходит как string;
  • BOOLEAN → может быть 0 | 1;
  • TIMESTAMP → строка ISO или UNIX time;
  • NULL → неопределённые значения.

Zod позволяет учитывать такие несоответствия через преобразования.

Приведение типов (coercion)

const ProductSchema = z.object({
  id: z.coerce.number(),
  price: z.coerce.number(),
  in_stock: z.coerce.boolean()
});

z.coerce автоматически приводит входные данные:

  • "123"123
  • "1"true
  • "0"false

Это особенно полезно при работе с драйверами, возвращающими строки.


Обработка nullable и optional значений

SQL допускает NULL, что требует явного отражения в схеме.

const CommentSchema = z.object({
  id: z.number(),
  text: z.string(),
  edited_at: z.string().datetime().nullable()
});

Разница между nullable() и optional():

  • nullable() — значение может быть null
  • optional() — значение может отсутствовать
  • комбинирование: nullable().optional() — отсутствует или null
const Schema = z.object({
  nickname: z.string().nullable().optional()
});

Преобразование данных после валидации

Zod позволяет не только проверять, но и нормализовать данные.

const OrderSchema = z.object({
  id: z.number(),
  total: z.string().transform((val) => Number(val)),
  created_at: z.string().transform((val) => new Date(val))
});

Результат:

  • вход: "99.50"
  • выход: 99.5 (number)

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


Валидация массивов данных из выборок

База данных часто возвращает списки записей. Для этого используется z.array.

const UserListSchema = z.array(UserSchema);

Применение:

const users = UserListSchema.parse(rowsFromDb);

Если хотя бы один элемент массива не соответствует схеме, валидация завершится ошибкой.


Частичная проверка (partial schemas)

При выборке через SELECT часто возвращается неполный набор полей. В таких случаях используется partial.

const UserPreviewSchema = UserSchema.partial();

Теперь все поля становятся необязательными:

  • полезно для SEL ECT id, email
  • удобно для API-слоёв, где данные урезаны

Также возможно комбинирование:

const Schema = UserSchema.pick({
  id: true,
  email: true
});

Композиция схем для сложных структур

При нормализации данных из нескольких таблиц используется композиция:

const ProfileSchema = z.object({
  bio: z.string(),
  avatar_url: z.string().url()
});

const UserWithProfileSchema = z.object({
  id: z.number(),
  email: z.string(),
  profile: ProfileSchema
});

Такая структура отражает join-операции SQL:

SELECT users.id, users.email, profile.bio, profile.avatar_url
FR OM users
JOIN profile ON profile.user_id = users.id;

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


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

При работе с внешними источниками или legacy-таблицами тип unknown становится стандартом входа.

const SafeUserSchema = z.object({
  id: z.unknown().transform((v) => Number(v)),
  email: z.string().email(),
  is_active: z.unknown().transform((v) => v === 1 || v === true)
});

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


Обработка ошибок валидации

Zod возвращает структурированные ошибки:

const result = UserSchema.safeParse(data);

Форма результата:

  • success: true → данные валидны
  • success: false → содержит error

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

  • путь до поля (path)
  • тип ошибки
  • сообщение

Пример обработки:

if (!result.success) {
  const issues = result.error.issues;
}

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


Интеграция с ORM и SQL-слоем

Zod часто используется поверх ORM или query builder’ов.

Prisma

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

const user = UserSchema.parse(prismaUser);

Raw SQL

const rows = await db.query("SEL ECT * FR OM users");

const users = z.array(UserSchema).parse(rows);

Такой слой обеспечивает защиту от неконсистентных данных, даже если схема БД изменена.


Дискриминируемые объединения для полиморфных таблиц

Иногда таблица содержит разные типы записей:

const EventSchema = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("click"),
    x: z.number(),
    y: z.number()
  }),
  z.object({
    type: z.literal("scroll"),
    scrollTop: z.number()
  })
]);

Это особенно полезно для event-логов, audit-таблиц и очередей задач.


Нормализация дат и временных меток

Базы данных часто возвращают даты в виде строк:

const LogSchema = z.object({
  created_at: z.string().transform((v) => new Date(v))
});

Дополнительно можно стандартизировать формат:

const Schema = z.object({
  created_at: z.coerce.date()
});

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


Валидация частично повреждённых данных

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

const LegacyUserSchema = z.object({
  id: z.coerce.number(),
  email: z.string().email().optional(),
  age: z.coerce.number().nullable()
});

Такой подход позволяет обрабатывать реальные данные без их предварительной очистки в БД.


Стратегии построения схем для базы данных

При проектировании схем Zod для БД обычно выделяются уровни:

  • Raw schema — соответствует сырым данным из SQL
  • Domain schema — нормализованные бизнес-объекты
  • DTO schema — данные для API или внешних интерфейсов

Пример разделения:

const RawUserSchema = z.object({
  id: z.unknown(),
  email: z.unknown(),
  created_at: z.unknown()
});

const DomainUserSchema = z.object({
  id: z.number(),
  email: z.string(),
  createdAt: z.date()
});

Паттерн “валидация на границе системы”

Основная идея использования Zod с базой данных заключается в том, что проверка выполняется:

  • сразу после получения данных;
  • до попадания в бизнес-логику;
  • до сериализации в API.

Это формирует строгую границу между:

  • неконтролируемыми источниками данных;
  • внутренними типизированными структурами приложения.