Работа с датами в JavaScript традиционно опирается на объект
Date, который хранит время в виде количества миллисекунд с
эпохи Unix и внутренне использует UTC-представление. Это приводит к
ключевой особенности: таймзона не хранится явно, а
вывод и парсинг могут давать разные результаты в зависимости от
окружения выполнения.
При построении схем валидации с использованием Zod возникает необходимость строго фиксировать правила работы с временными значениями: формат входных данных, наличие смещения, допустимость таймзоны и способ преобразования в внутреннее представление.
Zod предоставляет несколько инструментов для работы с датами:
z.date() — проверка, что значение является экземпляром
Datez.coerce.date() — преобразование строки или числа в
Datez.string().datetime() — проверка ISO 8601 строкиКаждый из этих подходов по-разному взаимодействует с временными зонами.
ISO 8601 является основным стандартом для передачи даты и времени:
2026-05-10T12:30:00Z
2026-05-10T12:30:00+06:00
2026-05-10T12:30:00.000+03:00
В Zod доступна строгая проверка:
import { z } from "zod";
const schema = z.string().datetime();
По умолчанию допускается формат с Z (UTC), но не
гарантируется наличие смещения.
Для работы с глобальными системами важно различать:
Z (UTC)+03:00, -06:00Zod позволяет явно требовать наличие offset:
const schema = z.string().datetime({ offset: true });
Это критично для систем, где локальное время без таймзоны считается ошибочным состоянием.
Объект Date не хранит информацию о временной зоне
исходной строки. Например:
new Date("2026-05-10T12:00:00+06:00")
После парсинга:
Zod при использовании z.coerce.date() не сохраняет
информацию о таймзоне:
const schema = z.coerce.date();
schema.parse("2026-05-10T12:00:00+06:00");
Результат — Date, нормализованный к UTC.
Наиболее распространённая модель:
Dateconst schema = z.coerce.date();
Эта модель упрощает вычисления, сортировку и сравнение.
Иногда требуется сохранить исходный offset как часть данных:
const schema = z.object({
datetime: z.string().datetime({ offset: true }),
offset: z.string().regex(/^[+-]\d{2}:\d{2}$/)
});
Здесь время и смещение разделяются, поскольку Date не
подходит для хранения исходного контекста.
Помимо offset существует система именованных зон:
Europe/BerlinAsia/AlmatyAmerica/New_YorkZod не предоставляет встроенной поддержки IANA таймзон, но позволяет валидировать их:
const tzSchema = z.string().refine((val) => {
try {
Intl.DateTimeFormat(undefined, { timeZone: val });
return true;
} catch {
return false;
}
});
Такая проверка опирается на Intl.
Во многих системах данные приходят раздельно:
{
"date": "2026-05-10",
"time": "12:30",
"timezone": "Asia/Almaty"
}
Схема Zod:
const schema = z.object({
date: z.string(),
time: z.string(),
timezone: z.string()
});
Дальнейшая сборка выполняется отдельно, поскольку Zod не занимается интерпретацией календарной логики.
z.preprocess позволяет нормализовать входные данные до
валидации:
const schema = z.preprocess((val) => {
if (typeof val === "string") {
return new Date(val);
}
return val;
}, z.date());
Для таймзонных строк это основной механизм:
const schema = z.preprocess((val) => {
if (typeof val !== "string") return val;
return new Date(val); // учитывает offset
}, z.date());
Однако даже здесь сохраняется ограничение: исходный offset теряется после преобразования.
Некоторые системы используют расширенные форматы:
2026-05-10T12:30:00+06:00[Asia/Almaty]2026-05-10T12:30:00Z/Europe/BerlinZod не распознаёт такие структуры напрямую. Они требуют ручного парсинга:
const schema = z.string().refine((val) => {
return /^.+(Z|[+-]\d{2}:\d{2})$/.test(val);
});
или более строгой обработки через разбор строки.
Используется для конвертации между зонами:
import { zonedTimeToUtc } from "date-fns-tz";
const schema = z.preprocess((val) => {
if (typeof val !== "string") return val;
return zonedTimeToUtc(val, "Asia/Almaty");
}, z.date());
Поддерживает полноценную модель DateTime:
import { DateTime } from "luxon";
const schema = z.preprocess((val) => {
return DateTime.fromISO(val).toJSDate();
}, z.date());
Здесь сохраняется логика таймзоны на этапе парсинга, но итоговое
значение всё равно становится Date.
Часто требуется проверка диапазонов времени с учётом таймзон:
const schema = z.object({
start: z.date(),
end: z.date()
}).refine((data) => {
return data.end > data.start;
});
Такая проверка выполняется уже после нормализации всех значений в UTC.
Существуют даты, которые неоднозначны:
Например:
2026-10-25 02:30 Europe/Berlin
Это время может существовать дважды или не существовать вовсе. Zod не решает эту проблему — она переносится в слой парсинга.
Для сложных систем применяется разделение типов:
ISODateTimeString — строка с offsetUTCDate — нормализованный DateTimeZoneId — строка IANALocalDateTime — строка без зоныПример:
const ISODateTimeString = z.string().datetime({ offset: true });
const EventSchema = z.object({
start: ISODateTimeString,
timezone: z.string()
});
Zod выполняет только:
Он не предоставляет:
Это переносится в специализированные библиотеки.
В архитектуре приложений выделяется отдельный этап:
Zod выполняет только первый этап, обеспечивая согласованность входных данных, но не интерпретацию временного контекста.