Строковые представления дат встречаются в большинстве прикладных систем: API, формы ввода, конфигурационные файлы, журналы событий. Основная проблема заключается в том, что строка сама по себе не несёт семантики времени и требует строгого преобразования в тип Date с контролем формата, временной зоны и допустимых значений.
Строка, описывающая дату, может иметь множество представлений:
2026-05-1010.05.202605/10/20262026-05-10T14:30:00Z2026-05-10 14:30Каждый формат несёт различную степень формальности и совместимости. В
JavaScript объект Date способен интерпретировать часть этих
строк автоматически, однако результат зависит от среды выполнения и не
гарантирует детерминированности.
Например:
new Date("10.05.2026") // может быть Invalid Date в большинстве сред
new Date("2026-05-10") // корректный ISO-формат
Отсутствие строгой валидации приводит к скрытым ошибкам: некорректные даты проходят проверку, но дают неожиданные результаты при обработке.
Библиотека Zod предоставляет несколько механизмов работы с датами, начиная от строгой проверки строк и заканчивая автоматическим преобразованием типов.
Основная цель — отделить валидацию от преобразования и зафиксировать поведение схемы явно.
z.coerce.date()Самый простой способ получить Date из строки —
использование коэрции:
import { z } from "zod";
const schema = z.coerce.date();
schema.parse("2026-05-10T12:00:00Z");
Поведение:
new Date()Invalid Date
отбрасывается)DateПреимущество подхода — минимальная сложность. Недостаток — отсутствие контроля над форматом строки.
Например, некорректные или неоднозначные строки могут быть приняты:
schema.parse("05/10/2026"); // зависит от окружения
Для API и систем, где требуется предсказуемость, применяется строгий разбор ISO 8601.
const schema = z.string().datetime();
Такой подход:
DateПример допустимого значения:
schema.parse("2026-05-10T14:30:00Z");
Пример ошибки:
schema.parse("10-05-2026"); // ошибка валидации
z.string().datetime() возвращает строку, а не
Date. Это принципиальное отличие от коэрции.
Для получения объекта Date используется связка:
const schema = z.string().datetime().transform((val) => new Date(val));
transformМеханизм transform позволяет отделить этап валидации от
этапа преобразования данных.
const schema = z
.string()
.datetime()
.transform((value) => new Date(value));
Такой подход обеспечивает:
При этом сохраняется явность: строка сначала проверяется, затем преобразуется.
Во многих системах встречаются локальные форматы дат, например:
10.05.202610-05-20262026/05/10Для таких случаев используется preprocess, позволяющий
изменить входные данные до валидации.
const schema = z.preprocess((val) => {
if (typeof val === "string") {
const [day, month, year] = val.split(".");
return `${year}-${month}-${day}`;
}
return val;
}, z.coerce.date());
Здесь выполняется нормализация строки в ISO-подобный формат, после
чего применяется стандартное преобразование в Date.
Подход позволяет:
Для более строгого контроля формата применяется комбинация
string().regex() и преобразования:
const schema = z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.transform((val) => new Date(val));
Такой подход:
Однако он не проверяет корректность самой даты (например,
2026-99-99 пройдёт regex).
Для устранения проблем с несуществующими датами используется
refine:
const schema = z
.string()
.datetime()
.refine((val) => !isNaN(new Date(val).getTime()), {
message: "Некорректная дата",
});
Хотя datetime() уже обеспечивает форматную проверку,
refine может применяться для дополнительных условий:
Пример ограничения диапазона:
const schema = z
.coerce.date()
.refine((date) => date >= new Date("2000-01-01"), {
message: "Дата слишком ранняя",
});
Одной из ключевых проблем при разборе строковых дат является интерпретация временной зоны.
Строка:
2026-05-10T00:00:00Z
является UTC-временем, тогда как:
2026-05-10T00:00:00
может интерпретироваться как локальное время.
При использовании new Date() происходит автоматическое
приведение к UTC внутри объекта, что может привести к смещению при
выводе.
Zod не выполняет нормализацию временных зон, поэтому ответственность за согласованность лежит на уровне схемы:
Zconst schema = z
.string()
.datetime()
.transform((val) => new Date(val + "Z"));
(подход применим только при гарантии отсутствия зоны во входных данных)
В реальных системах часто используется комбинированная схема:
const schema = z.object({
createdAt: z.string().datetime().transform((v) => new Date(v)),
updatedAt: z.coerce.date(),
});
Здесь демонстрируется различие подходов:
Такое разделение позволяет контролировать уровень доверия к данным.
Zod возвращает структурированные ошибки, которые позволяют точно определить причину сбоя:
Пример:
try {
schema.parse("2026-13-40T00:00:00Z");
} catch (e) {
console.log(e.errors);
}
Ошибка включает путь до поля, ожидаемый формат и фактическое значение.
На практике встречаются устойчивые проблемы:
Date и string в одной моделиnew Date() без проверкиОсобенно критичной является ситуация, когда разные клиенты отправляют разные форматы одной и той же сущности.
Наиболее устойчивые подходы сводятся к нескольким моделям:
z.string().datetime().transform((v) => new Date(v));
z.coerce.date();
z.preprocess(normalize, z.coerce.date());
z.coerce.date().refine(isBusinessValidDate);
Каждый вариант отражает различный уровень доверия к входным данным и требования к строгости.
Выбор стратегии зависит от характера системы:
Разделение уровней обработки позволяет избежать неоднозначностей и упрощает поддержку схем.