OpenAPI спецификации

OpenAPI спецификация описывает контракт HTTP API через формализованную схему: маршруты, параметры, тела запросов и ответы. Основная проблема при работе с такими контрактами заключается в необходимости синхронизации описания API и фактической валидации данных в рантайме. Именно здесь структурная валидация с использованием Superstruct становится практическим инструментом, позволяющим интерпретировать OpenAPI схемы как исполняемые проверки данных.

Superstruct оперирует композиционными структурами, где каждая сущность описывает правила проверки входных данных. OpenAPI 3.x, в свою очередь, описывает JSON Schema-подобные конструкции. Прямое соответствие между этими моделями не всегда очевидно, но может быть выстроено через трансляцию типов.


Базовое сопоставление типов OpenAPI и Superstruct

OpenAPI использует ограниченный набор примитивов:

  • string
  • number / integer
  • boolean
  • object
  • array
  • null (через nullable или union)

Superstruct строит аналогичную систему через фабрики структур:

  • string() → строка
  • number() → число с проверкой NaN
  • boolean() → булево значение
  • object({…}) → объект с описанной формой
  • array(struct) → массив элементов заданного типа

Простейшая трансляция выглядит линейной:

OpenAPI:

type: string

Superstruct:

import { string } fr om 'superstruct';

const Username = string();

Однако реальная сложность возникает при обработке ограничений.


Ограничения и валидационные правила

OpenAPI допускает множество дополнительных модификаторов:

  • minLength / maxLength
  • minimum / maximum
  • pattern
  • enum
  • format

Superstruct реализует это через композицию и декораторы проверок.

Пример сопоставления:

OpenAPI:

type: string
minLength: 3
maxLength: 20
pattern: "^[a-zA-Z]+$"

Superstruct:

import { string, size, pattern } fr om 'superstruct';

const Username = pattern(
  size(string(), 3, 20),
  /^[a-zA-Z]+$/
);

Здесь важно, что Superstruct не хранит метаданные схемы, а выполняет только проверку, поэтому трансформация OpenAPI → Superstruct является однонаправленной.


Объекты и схема properties

Основной структурой в OpenAPI является object с набором свойств.

OpenAPI:

type: object
properties:
  id:
    type: integer
  name:
    type: string
required: [id, name]

Superstruct:

import { object, number, string } from 'superstruct';

const User = object({
  id: number(),
  name: string()
});

Обязательность полей в Superstruct задаётся фактом отсутствия optional-обёртки. Для OpenAPI optional поля необходимо явно оборачивать.

import { object, number, string, optional } from 'superstruct';

const User = object({
  id: number(),
  name: string(),
  age: optional(number())
});

Массивы и вложенные структуры

OpenAPI описывает массивы через items.

type: array
items:
  type: string

Superstruct:

import { array, string } from 'superstruct';

const Tags = array(string());

Вложенные структуры транслируются рекурсивно:

type: object
properties:
  tags:
    type: array
    items:
      type: object
      properties:
        label:
          type: string
const Tag = object({
  label: string()
});

const Model = object({
  tags: array(Tag)
});

oneOf, anyOf и дисъюнктивные типы

OpenAPI поддерживает сложные композиции:

  • oneOf — строго один вариант
  • anyOf — любой набор
  • allOf — объединение

Superstruct реализует аналог через union и intersection.

oneOf

oneOf:
  - type: string
  - type: number
import { union, string, number } from 'superstruct';

const Value = union([string(), number()]);

allOf

allOf:
  - type: object
    properties:
      id:
        type: number
  - type: object
    properties:
      name:
        type: string
import { intersection, object, number, string } from 'superstruct';

const A = object({ id: number() });
const B = object({ name: string() });

const Model = intersection([A, B]);

Nullable и optional значения

OpenAPI 3.x допускает nullable:

type: string
nullable: true

В Superstruct это выражается через union с null:

import { union, string, literal } from 'superstruct';

const NullableString = union([string(), literal(null)]);

Enum значения

OpenAPI:

type: string
enum: [pending, active, blocked]

Superstruct:

import { enums } from 'superstruct';

const Status = enums(['pending', 'active', 'blocked']);

Enum в Superstruct строго ограничивает множество допустимых значений, что полностью соответствует семантике OpenAPI.


Форматы (format)

OpenAPI часто использует format:

  • email
  • uuid
  • date-time

Superstruct не навязывает строгую семантику форматов, поэтому они реализуются через custom refinement.

import { string, refine } from 'superstruct';

const Email = refine(string(), 'Email', (value) =>
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
);

Для UUID:

const UUID = refine(string(), 'UUID', (value) =>
  /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
);

Параметры запросов и requestBody

OpenAPI различает:

  • path parameters
  • query parameters
  • headers
  • body

Каждый тип может быть преобразован в отдельную структуру.

Query параметры

in: query
name: lim it
schema:
  type: integer
const Query = object({
  lim it: optional(number())
});

Request body

requestBody:
  content:
    application/json:
      schema:
        type: object
        properties:
          title:
            type: string
const Body = object({
  title: string()
});

Обработка additionalProperties

OpenAPI:

type: object
additionalProperties: false

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

import { object, Struct } from 'superstruct';

function strict(struct) {
  return new Struct({
    ...struct,
    validator: (value) => {
      const keys = Object.keys(value);
      const allowed = Object.keys(struct.schema);
      return keys.every((k) => allowed.includes(k));
    }
  });
}

Наследование схем через компоненты OpenAPI

OpenAPI Components позволяют переиспользовать схемы:

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer

В Superstruct это выражается через композицию модулей:

const User = object({
  id: number()
});

const Post = object({
  author: User,
  title: string()
});

Генерация структур из OpenAPI

Практический подход заключается в обходе AST OpenAPI документа и генерации Superstruct-схем.

Логика трансляции:

  1. type → базовый struct
  2. properties → object()
  3. items → array()
  4. enum → enums()
  5. oneOf → union()
  6. allOf → intersection()

Упрощённый генератор:

function compile(schema) {
  switch (schema.type) {
    case 'string':
      return string();
    case 'number':
    case 'integer':
      return number();
    case 'boolean':
      return boolean();
    case 'array':
      return array(compile(schema.items));
    case 'object':
      const props = {};
      for (const key in schema.properties) {
        props[key] = compile(schema.properties[key]);
      }
      return object(props);
  }

  if (schema.enum) {
    return enums(schema.enum);
  }

  if (schema.oneOf) {
    return union(schema.oneOf.map(compile));
  }

  if (schema.allOf) {
    return intersection(schema.allOf.map(compile));
  }
}

Валидация HTTP ответов

OpenAPI часто используется не только для входных данных, но и для проверки ответов API.

const Response = object({
  data: array(User),
  meta: object({
    total: number()
  })
});

function validateResponse(data) {
  return create(data, Response);
}

Это позволяет выявлять рассинхронизацию API-контракта и реализации сервера.


Middleware-слой интеграции

Superstruct легко интегрируется в HTTP-слои:

  • Express
  • Fastify
  • Koa

Пример middleware:

function validateBody(struct) {
  return (req, res, next) => {
    try {
      req.body = create(req.body, struct);
      next();
    } catch (e) {
      res.status(400).send(e.message);
    }
  };
}

Ограничения трансляции OpenAPI в Superstruct

Несмотря на структурное соответствие, существует ряд ограничений:

  • OpenAPI поддерживает более сложные JSON Schema конструкции
  • Superstruct не хранит метаданные схем
  • отсутствует native support для $ref resolution
  • различия в обработке default значений
  • отсутствует формальная поддержка dependentRequired

Это делает Superstruct не заменой OpenAPI, а runtime-слоем поверх него.


Подход к архитектуре валидации

На практике применяется трёхуровневая модель:

  1. OpenAPI — контракт
  2. Superstruct — runtime validation
  3. TypeScript — compile-time типизация

Такая комбинация позволяет синхронизировать:

  • документацию
  • runtime безопасность
  • статическую типизацию

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