Работа с динамическими структурами данных валидации становится критически важной при проектировании API, схем обмена данными и конфигурируемых систем. Библиотека Superstruct предоставляет минималистичный, но гибкий набор примитивов для описания структур и проверки данных, однако её потенциал существенно расширяется при переходе к генерации схем непосредственно из существующих данных.
Первый этап построения схемы на основе данных заключается в выявлении устойчивых паттернов внутри выборки значений. В типичных сценариях вход представляет собой JSON-массив объектов, где каждая запись может отличаться по набору полей и типам значений.
Основные наблюдаемые характеристики:
Простейший подход начинается с прохода по выборке и агрегирования метаданных о каждом поле.
const sample = [
{ id: 1, name: "Alice", age: 25 },
{ id: 2, name: "Bob", age: "26" },
{ id: 3, name: "Charlie" }
];
На уровне анализа формируется карта типов:
Такая карта становится промежуточным представлением перед построением схемы Superstruct.
После извлечения метаданных выполняется трансляция в примитивы
валидации. В Superstruct используются базовые структуры:
string, number, boolean,
array, object, а также композиции через
union, optional, nullable.
Алгоритм преобразования:
Пример генерации:
import { object, string, number, union, optional } from "superstruct";
const schema = object({
id: number(),
name: string(),
age: optional(union([number(), string()]))
});
Если поле отсутствует в части записей, оно автоматически становится
optional. Если наблюдается несколько типов, используется
union.
Ключевой этап генерации схем — детектирование типа на основе JavaScript-значения.
Базовая функция классификации:
function detectType(value) {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
if (typeof value === "number") return "number";
if (typeof value === "string") return "string";
if (typeof value === "boolean") return "boolean";
if (typeof value === "object") return "object";
return "unknown";
}
Расширенная версия учитывает:
Например, строка "2024-01-01" может интерпретироваться
как дата, если применяется дополнительный слой эвристики.
При работе с массивом объектов выполняется накопление информации о каждом ключе.
Структура накопителя:
const registry = {
age: new Set(),
name: new Set(),
id: new Set()
};
Проход по данным:
for (const item of sample) {
for (const key in item) {
registry[key].add(detectType(item[key]));
}
}
Результат:
На основании этого строится финальная схема.
При наличии вложенных структур процесс рекурсивно повторяется. Каждый объект рассматривается как самостоятельная схема.
Пример входных данных:
const sample = [
{
id: 1,
profile: {
email: "a@test.com",
active: true
}
},
{
id: 2,
profile: {
email: "b@test.com"
}
}
];
Результирующая схема:
const schema = object({
id: number(),
profile: object({
email: string(),
active: optional(boolean())
})
});
Рекурсивный обход позволяет строить произвольно глубокие структуры без заранее заданного контракта.
Массивы представляют сложность, если их элементы различаются по форме.
Пример:
const sample = [
{ type: "a", value: 1 },
{ type: "b", value: "text" },
{ type: "c", value: true }
];
Возможные стратегии генерации схемы:
const schema = array(
union([
object({ type: string(), value: number() }),
object({ type: string(), value: string() }),
object({ type: string(), value: boolean() })
])
);
const schema = array(
object({
type: string(),
value: union([number(), string(), boolean()])
})
);
Вторая стратегия предпочтительнее при высокой вариативности данных.
Поле считается обязательным, если:
Формально:
function isRequired(key, dataset) {
return dataset.every(item => item[key] !== undefined);
}
Если условие не выполняется, используется optional() в
Superstruct.
Финальная схема представляет собой результат композиции примитивов.
Поддерживаются операции:
union)object)array)optional)Пример композиции:
const schema = object({
id: number(),
tags: array(string()),
meta: optional(object({
createdAt: string(),
updatedAt: string()
}))
});
В реальных системах данные часто содержат смесь типизированных и нетипизированных значений.
Пример:
[
{ id: "1", value: 10 },
{ id: 2, value: "20" }
]
Алгоритм обработки:
Результат:
const schema = array(
object({
id: union([string(), number()]),
value: union([number(), string()])
})
);
При наличии больших выборок полезно учитывать распределение типов, а не только их факт существования.
Пример:
В таких случаях допускается стратегия приоритизации:
number() // основной тип
union([number(), string()]) // при наличии выбросов
Генерация схем из данных фактически является обратной задачей по отношению к сериализации: вместо описания структуры заранее выполняется её реконструкция.
Эта логика позволяет:
Superstruct при этом выступает как финальный слой формализации, фиксирующий результат анализа в виде строгой декларативной модели.