Работа с унаследованными форматами данных требует не только проверки корректности, но и приведения входящих значений к современным моделям. В Superstruct такие задачи решаются через комбинацию механизмов валидации, преобразования и обогащения структур, что позволяет постепенно мигрировать системы без разрушения обратной совместимости.
Старые данные часто имеют следующие особенности:
"42" вместо
42)user_name вместо
username)"0" / "1" или
"yes" / "no"При прямой валидации такие данные не проходят проверку, хотя логически остаются валидными.
Superstruct решает это через слой трансформации перед или во время проверки.
Трансформация в контексте Superstruct — это приведение входного значения к ожидаемой форме до применения строгих правил проверки.
Ключевые инструменты:
coerce — автоматическое преобразование типовdefaulted — заполнение отсутствующих значенийpreprocess — произвольная функция нормализации
входаobject,
array, unionpreprocess для нормализации входа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 автоматически определяет подходящую ветку.
В реальных системах трансформация выполняется слоями:
preprocess)Пример композиции:
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),
});
Такой подход позволяет сохранять максимум информации без отказа всей структуры.
В серверных приложениях трансформация данных обычно располагается на границе системы:
Superstruct позволяет унифицировать эти точки входа через единый слой структур.