Парсинг строк в даты

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

Строка, описывающая дату, может иметь множество представлений:

  • 2026-05-10
  • 10.05.2026
  • 05/10/2026
  • 2026-05-10T14:30:00Z
  • 2026-05-10 14:30

Каждый формат несёт различную степень формальности и совместимости. В JavaScript объект Date способен интерпретировать часть этих строк автоматически, однако результат зависит от среды выполнения и не гарантирует детерминированности.

Например:

new Date("10.05.2026") // может быть Invalid Date в большинстве сред
new Date("2026-05-10")  // корректный ISO-формат

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

Базовая модель преобразования в Zod

Библиотека 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"); // зависит от окружения

Строгая проверка ISO-формата

Для API и систем, где требуется предсказуемость, применяется строгий разбор ISO 8601.

const schema = z.string().datetime();

Такой подход:

  • проверяет соответствие формату ISO 8601
  • отклоняет произвольные строки
  • не выполняет преобразование в 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.2026
  • 10-05-2026
  • 2026/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

Для устранения проблем с несуществующими датами используется 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 не выполняет нормализацию временных зон, поэтому ответственность за согласованность лежит на уровне схемы:

  • фиксированный ISO с Z
  • или явная конвертация через transform
const schema = z
  .string()
  .datetime()
  .transform((val) => new Date(val + "Z"));

(подход применим только при гарантии отсутствия зоны во входных данных)

Комбинированные схемы для API

В реальных системах часто используется комбинированная схема:

const schema = z.object({
  createdAt: z.string().datetime().transform((v) => new Date(v)),
  updatedAt: z.coerce.date(),
});

Здесь демонстрируется различие подходов:

  • строгая строковая валидация с явным ISO
  • гибкая коэрция для внешних источников

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

Ошибки парсинга и их структура

Zod возвращает структурированные ошибки, которые позволяют точно определить причину сбоя:

  • invalid_string
  • invalid_date
  • invalid_type

Пример:

try {
  schema.parse("2026-13-40T00:00:00Z");
} catch (e) {
  console.log(e.errors);
}

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

Типичные ошибки при работе со строковыми датами

На практике встречаются устойчивые проблемы:

  • использование локальных форматов без нормализации
  • отсутствие учёта временной зоны
  • смешение Date и string в одной модели
  • доверие к new Date() без проверки
  • отсутствие ограничения диапазонов

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

Рекомендованные паттерны построения схем

Наиболее устойчивые подходы сводятся к нескольким моделям:

Строгий ISO + transform

z.string().datetime().transform((v) => new Date(v));

Коэрция без контроля формата

z.coerce.date();

Нормализация через preprocess

z.preprocess(normalize, z.coerce.date());

Гибрид с бизнес-валидацией

z.coerce.date().refine(isBusinessValidDate);

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

Семантика выбора подхода

Выбор стратегии зависит от характера системы:

  • API между сервисами — предпочтение ISO + transform
  • пользовательский ввод — preprocess + коэрция
  • внутренние модели — coerce.date()
  • критичные системы времени — строгая валидация + refine + диапазоны

Разделение уровней обработки позволяет избежать неоднозначностей и упрощает поддержку схем.