Десериализация ответов

Работа с временными данными в клиент-серверных приложениях почти всегда включает этап преобразования «сырых» ответов API в структуры, пригодные для дальнейшей обработки. В JavaScript этим промежуточным слоем часто выступают строки формата ISO 8601, Unix timestamps или нестандартизированные представления дат. Библиотека Luxon предоставляет строгую модель работы со временем через объект DateTime, что делает десериализацию ключевым этапом интеграции с внешними источниками данных.

Форматы временных данных в ответах API

Типичный JSON-ответ может содержать даты в различных форматах:

  • ISO 8601 строки: "2026-05-23T14:30:00Z"
  • Unix timestamp в секундах: 1716479400
  • Unix timestamp в миллисекундах: 1716479400000
  • Локальные строки без зоны: "2026-05-23 14:30:00"
  • RFC 2822: "Sat, 23 May 2026 14:30:00 GMT"

Каждый формат требует собственного подхода при преобразовании в DateTime.

Luxon не выполняет автоматическую десериализацию JSON, поэтому преобразование выполняется вручную или через промежуточные механизмы (например, reviver в JSON.parse).


Базовая десериализация ISO строк

Наиболее надёжный формат для обмена временем — ISO 8601. Luxon поддерживает его напрямую:

import { DateTime } from "luxon";

const json = `{
  "createdAt": "2026-05-23T14:30:00Z"
}`;

const data = JSON.parse(json);

const createdAt = DateTime.fromISO(data.createdAt);

console.log(createdAt.toString());

Метод fromISO автоматически определяет:

  • временную зону (Z → UTC)
  • наличие миллисекунд
  • корректность формата

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


Десериализация Unix timestamp

Многие API возвращают время в виде числовых меток.

Секунды

const timestamp = 1716479400;

const dt = DateTime.fromSeconds(timestamp);

Миллисекунды

const timestampMs = 1716479400000;

const dt = DateTime.fromMillis(timestampMs);

Разделение этих двух случаев критично: ошибка в единицах измерения приводит к смещению даты на десятки тысяч лет.


Унификация входных форматов

Реальные API часто смешивают форматы. Для этого создаётся слой нормализации:

function parseDate(value) {
  if (typeof value === "string") {
    return DateTime.fromISO(value);
  }

  if (typeof value === "number") {
    if (value < 1e12) {
      return DateTime.fromSeconds(value);
    }
    return DateTime.fromMillis(value);
  }

  return null;
}

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


Использование JSON reviver для автоматической десериализации

JSON.parse поддерживает функцию преобразования значений:

const json = `{
  "createdAt": "2026-05-23T14:30:00Z",
  "updatedAt": "2026-05-22T10:00:00Z"
}`;

const data = JSON.parse(json, (key, value) => {
  if (key.toLowerCase().includes("at") && typeof value === "string") {
    return DateTime.fromISO(value);
  }
  return value;
});

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

Важно учитывать ограничение: reviver не различает семантику поля, поэтому эвристики по имени ключа должны быть строго определены.


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

Некоторые API используют локальные форматы дат, не совместимые с ISO.

RFC 2822

const dt = DateTime.fromRFC2822("Sat, 23 May 2026 14:30:00 GMT");

SQL datetime

const dt = DateTime.fromSQL("2026-05-23 14:30:00");

Luxon поддерживает SQL-формат без дополнительного парсинга, что полезно при работе с legacy-бэкендами.


Обработка временных зон при десериализации

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

const dt = DateTime.fromISO("2026-05-23T14:30:00", {
  zone: "utc"
});

Варианты поведения:

  • явное указание зоны через параметр zone
  • использование setZone: true при необходимости сохранения исходной зоны
  • нормализация к UTC для хранения и сравнения
const utc = DateTime.fromISO(value).toUTC();

Стандартизация к UTC часто используется как промежуточный слой при десериализации API-ответов.


Гидратация объектов API

API-ответы обычно содержат смешанные структуры:

const response = {
  id: 1,
  name: "Event",
  createdAt: "2026-05-23T14:30:00Z",
  schedule: {
    start: "2026-05-24T09:00:00Z",
    end: "2026-05-24T11:00:00Z"
  }
};

Для преобразования в «обогащённую» модель применяется гидратация:

function hydrateEvent(data) {
  return {
    ...data,
    createdAt: DateTime.fromISO(data.createdAt),
    schedule: {
      start: DateTime.fromISO(data.schedule.start),
      end: DateTime.fromISO(data.schedule.end)
    }
  };
}

Такой слой отделяет сетевой формат от доменной модели приложения.


Ленивое преобразование (lazy deserialization)

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

class EventModel {
  constructor(raw) {
    this.raw = raw;
  }

  get createdAt() {
    return DateTime.fromISO(this.raw.createdAt);
  }
}

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


Кэширование результатов десериализации

При частых обращениях к одним и тем же данным создаётся кэш:

class EventModel {
  constructor(raw) {
    this.raw = raw;
    this._createdAt = DateTime.fromISO(raw.createdAt);
  }

  get createdAt() {
    return this._createdAt;
  }
}

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


Обработка некорректных данных

Luxon возвращает объект Invalid DateTime при ошибках парсинга.

const dt = DateTime.fromISO("invalid-date");

if (!dt.isValid) {
  console.log(dt.invalidReason);
}

При десериализации это состояние требует централизованной обработки:

function safeParseISO(value) {
  const dt = DateTime.fromISO(value);
  return dt.isValid ? dt : null;
}

Нормализация данных после десериализации

После преобразования данные часто приводятся к единому формату:

  • UTC для хранения
  • локальное время для отображения
  • ISO строки для передачи дальше
const normalized = {
  createdAt: DateTime.fromISO(data.createdAt).toUTC().toISO(),
  localView: DateTime.fromISO(data.createdAt).toLocal()
};

Интеграция с fetch и axios

При работе с сетевыми библиотеками десериализация часто выносится в interceptor.

fetch

async function fetchEvent(id) {
  const res = await fetch(`/api/events/${id}`);
  const data = await res.json();

  return hydrateEvent(data);
}

axios

axios.interceptors.response.use((response) => {
  if (response.data.createdAt) {
    response.data.createdAt = DateTime.fromISO(response.data.createdAt);
  }
  return response;
});

Глубокая десериализация вложенных структур

Для сложных JSON используется рекурсивный обход:

function deepParseDates(obj) {
  if (Array.isArray(obj)) {
    return obj.map(deepParseDates);
  }

  if (obj && typeof obj === "object") {
    const result = {};

    for (const key in obj) {
      const value = obj[key];

      if (typeof value === "string" && key.includes("At")) {
        result[key] = DateTime.fromISO(value);
      } else {
        result[key] = deepParseDates(value);
      }
    }

    return result;
  }

  return obj;
}

Согласование клиентского и серверного времени

При десериализации важно учитывать различие между:

  • временем генерации на сервере
  • временем получения на клиенте
  • локальным временем пользователя

Luxon позволяет фиксировать каждую из этих точек:

const serverTime = DateTime.fromISO(data.createdAt, { zone: "utc" });
const receivedAt = DateTime.now();

Это создаёт основу для анализа задержек и синхронизации состояния.


Особенности сериализации обратно в JSON

Хотя тема сосредоточена на десериализации, важно учитывать обратное преобразование:

const payload = {
  createdAt: DateTime.now().toISO()
};

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


Типизация десериализованных данных

В TypeScript часто отделяются «сырые» и «обогащённые» модели:

type ApiEvent = {
  createdAt: string;
};

type DomainEvent = {
  createdAt: DateTime;
};

Функция преобразования становится контрактом между слоями:

function toDomain(data: ApiEvent): DomainEvent {
  return {
    createdAt: DateTime.fromISO(data.createdAt)
  };
}

Производительность при массовой десериализации

При обработке больших массивов событий:

const events = data.map(e => ({
  ...e,
  createdAt: DateTime.fromISO(e.createdAt)
}));

Ключевыми факторами становятся:

  • избегание повторного парсинга
  • минимизация рекурсивных обходов
  • использование кэширования при повторных данных
  • предварительная нормализация формата на сервере

Стабильность и детерминированность преобразований

Десериализация должна быть:

  • детерминированной (одинаковый вход → одинаковый результат)
  • независимой от локали
  • устойчивой к частичной некорректности данных

Luxon обеспечивает эти свойства через явные методы fromISO, fromMillis, fromSeconds, что исключает скрытую магию преобразования и делает поведение предсказуемым в распределённых системах