Генерация схем из данных

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

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

Основные наблюдаемые характеристики:

  • наличие обязательных и опциональных полей
  • вариативность типов внутри одного ключа
  • вложенность объектов
  • массивы неоднородной структуры
  • значения, допускающие null или undefined

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

const sample = [
  { id: 1, name: "Alice", age: 25 },
  { id: 2, name: "Bob", age: "26" },
  { id: 3, name: "Charlie" }
];

На уровне анализа формируется карта типов:

  • id → number
  • name → string
  • age → number | string | отсутствует

Такая карта становится промежуточным представлением перед построением схемы Superstruct.

Преобразование типовой карты в структуру Superstruct

После извлечения метаданных выполняется трансляция в примитивы валидации. В Superstruct используются базовые структуры: string, number, boolean, array, object, а также композиции через union, optional, nullable.

Алгоритм преобразования:

  1. Определение доминирующего типа
  2. Проверка конфликтов типов
  3. Оборачивание в union при неоднозначности
  4. Пометка отсутствующих полей как optional

Пример генерации:

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";
}

Расширенная версия учитывает:

  • числовые строки (“123”)
  • ISO даты
  • UUID
  • пустые строки
  • вложенные структуры

Например, строка "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]));
  }
}

Результат:

  • age → {number, string}
  • name → {string}
  • id → {number}

На основании этого строится финальная схема.

Генерация вложенных объектов

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

Пример входных данных:

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 }
];

Возможные стратегии генерации схемы:

1. Унифицированный объект через union

const schema = array(
  union([
    object({ type: string(), value: number() }),
    object({ type: string(), value: string() }),
    object({ type: string(), value: boolean() })
  ])
);

2. Обобщённый тип поля

const schema = array(
  object({
    type: string(),
    value: union([number(), string(), boolean()])
  })
);

Вторая стратегия предпочтительнее при высокой вариативности данных.

Обнаружение обязательных и опциональных полей

Поле считается обязательным, если:

  • присутствует во всех объектах
  • отсутствует значение null/undefined
  • не демонстрирует пропусков в выборке

Формально:

function isRequired(key, dataset) {
  return dataset.every(item => item[key] !== undefined);
}

Если условие не выполняется, используется optional() в Superstruct.

Построение схемы через композицию структур

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

Поддерживаются операции:

  • объединение (union)
  • вложенность (object)
  • массивы (array)
  • опциональность (optional)
  • преобразования (coercion-логика на внешнем уровне)

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

const schema = object({
  id: number(),
  tags: array(string()),
  meta: optional(object({
    createdAt: string(),
    updatedAt: string()
  }))
});

Генерация схем из частично типизированных данных

В реальных системах данные часто содержат смесь типизированных и нетипизированных значений.

Пример:

[
  { id: "1", value: 10 },
  { id: 2, value: "20" }
]

Алгоритм обработки:

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

Результат:

const schema = array(
  object({
    id: union([string(), number()]),
    value: union([number(), string()])
  })
);

Построение схемы с учётом частоты типов

При наличии больших выборок полезно учитывать распределение типов, а не только их факт существования.

Пример:

  • 95% значений — number
  • 5% значений — string

В таких случаях допускается стратегия приоритизации:

  • доминирующий тип становится базовым
  • остальные добавляются в union
number() // основной тип
union([number(), string()]) // при наличии выбросов

Генерация схем как обратная задача сериализации

Генерация схем из данных фактически является обратной задачей по отношению к сериализации: вместо описания структуры заранее выполняется её реконструкция.

Эта логика позволяет:

  • строить схемы для неизвестных API
  • документировать legacy-данные
  • автоматически валидировать внешние источники
  • адаптировать структуру под изменяющиеся форматы

Superstruct при этом выступает как финальный слой формализации, фиксирующий результат анализа в виде строгой декларативной модели.