Трансформация старых данных

Работа с унаследованными форматами данных требует не только проверки корректности, но и приведения входящих значений к современным моделям. В Superstruct такие задачи решаются через комбинацию механизмов валидации, преобразования и обогащения структур, что позволяет постепенно мигрировать системы без разрушения обратной совместимости.

Проблема несовместимых схем данных

Старые данные часто имеют следующие особенности:

  • числовые значения представлены строками ("42" вместо 42)
  • отсутствуют новые обязательные поля
  • изменены названия ключей (user_name вместо username)
  • вложенные структуры частично «развёрнуты»
  • булевые значения кодируются через "0" / "1" или "yes" / "no"

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

Superstruct решает это через слой трансформации перед или во время проверки.

Базовая идея трансформации

Трансформация в контексте Superstruct — это приведение входного значения к ожидаемой форме до применения строгих правил проверки.

Ключевые инструменты:

  • coerce — автоматическое преобразование типов
  • defaulted — заполнение отсутствующих значений
  • preprocess — произвольная функция нормализации входа
  • комбинирование структур через object, array, union

Использование preprocess для нормализации входа

preprocess позволяет изменить входные данные до валидации структуры.

Пример: приведение строковых чисел к числам

import { struct, number, preprocess } from "superstruct";

const NumberFromString = preprocess((value) => {
  if (typeof value === "string" && value.trim() !== "") {
    const parsed = Number(value);
    return Number.isNaN(parsed) ? value : parsed;
  }
  return value;
}, number);

NumberFromString.assert("42"); // 42

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

Преобразование устаревших объектов

Рассмотрим миграцию структуры пользователя:

Старый формат:

{
  "user_name": "alex",
  "age": "30",
  "is_active": "1"
}

Новый формат:

{
  "username": "alex",
  "age": 30,
  "active": true
}

Реализация трансформации:

import { object, string, number, boolean, preprocess } from "superstruct";

const toBoolean = preprocess((value) => {
  if (value === "1" || value === 1 || value === "true") return true;
  if (value === "0" || value === 0 || value === "false") return false;
  return value;
}, boolean);

const User = object({
  username: preprocess((value, obj) => obj.user_name ?? value, string),
  age: preprocess((value) => Number(value), number),
  active: preprocess((value) => toBoolean(value), boolean),
});

Здесь трансформация выполняет роль адаптера между схемами.

Переименование ключей

Переименование полей — частая задача при миграции.

Подход через preprocess на уровне объекта:

const renameKeys = preprocess((value) => {
  if (!value || typeof value !== "object") return value;

  return {
    username: value.user_name,
    age: value.age,
    active: value.is_active,
  };
}, object({
  username: string(),
  age: number(),
  active: boolean(),
}));

Такой слой позволяет полностью изолировать старый формат.

Значения по умолчанию через defaulted

При миграции часто появляются новые обязательные поля.

import { defaulted } from "superstruct";

const User = object({
  username: string(),
  age: number(),
  role: defaulted(string(), "user"),
});

Если поле отсутствует в старых данных, оно автоматически добавляется.

Обогащение данных во время трансформации

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

const enrichUser = preprocess((value) => {
  if (!value) return value;

  return {
    ...value,
    isAdult: Number(value.age) >= 18,
  };
}, object({
  age: number(),
  isAdult: boolean(),
}));

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

Работа с массивами устаревших структур

Массивы требуют трансформации каждого элемента.

import { array } from "superstruct";

const LegacyUser = object({
  user_name: string(),
  age: string(),
});

const NewUser = object({
  username: string(),
  age: number(),
});

const Users = array(
  preprocess((items) => {
    if (!Array.isArray(items)) return items;

    return items.map((u) => ({
      username: u.user_name,
      age: Number(u.age),
    }));
  }, NewUser)
);

Каждый элемент приводится к новой схеме до проверки.

Условная трансформация через union

Когда старый и новый формат сосуществуют, используется объединение схем.

const NewFormat = object({
  username: string(),
  age: number(),
});

const LegacyFormat = preprocess((value) => ({
  username: value.user_name,
  age: Number(value.age),
}), NewFormat);

const User = union([NewFormat, LegacyFormat]);

Superstruct автоматически определяет подходящую ветку.

Постепенная миграция схем

В реальных системах трансформация выполняется слоями:

  1. нормализация входа (preprocess)
  2. переименование ключей
  3. приведение типов
  4. добавление значений по умолчанию
  5. финальная валидация структуры

Пример композиции:

const User = preprocess(normalizeLegacyUser,
  object({
    username: string(),
    age: number(),
    active: boolean(),
  })
);

Где normalizeLegacyUser объединяет все шаги миграции.

Разделение ответственности трансформации

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

  • слой адаптации входных данных
  • слой доменной валидации
  • слой бизнес-логики

Superstruct используется преимущественно в первых двух слоях, обеспечивая предсказуемую форму данных до попадания в бизнес-логику.

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

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

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

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

Комбинирование трансформаций

Superstruct позволяет строить цепочки преобразований:

const normalize = preprocess(step1,
  preprocess(step2,
    preprocess(step3, finalStruct)
  )
);

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

Работа с частично валидными данными

При миграции часто встречаются «грязные» данные, где часть полей корректна, а часть нет.

const SafeUser = object({
  username: defaulted(string(), "unknown"),
  age: defaulted(number(), 0),
});

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

Использование трансформаций в API-слое

В серверных приложениях трансформация данных обычно располагается на границе системы:

  • входящие HTTP-запросы
  • ответы внешних API
  • данные из очередей сообщений

Superstruct позволяет унифицировать эти точки входа через единый слой структур.