Контекстно-зависимая валидация

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

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


Контекст в Superstruct — это дополнительный объект, передаваемый в процесс валидации. Он не является частью проверяемых данных, но влияет на то, какие правила применяются.

Типичный контекст может включать:

  • роль пользователя (role)
  • режим операции (mode: create/update/read)
  • флаги окружения (isAdmin, isDebug)
  • метаданные запроса (source, locale)

Пример передачи контекста:

import { validate } from 'superstruct'

validate(data, struct, {
  coerce: true,
  context: {
    role: 'admin',
    mode: 'create'
  }
})

Контекст становится дополнительным аргументом, который может использоваться внутри кастомных структур и проверок.


Базовая идея условных структур

Обычные структуры Superstruct описывают фиксированную форму данных:

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

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

Однако в контекстно-зависимых сценариях этого недостаточно. Например, поле age может быть обязательным только при создании пользователя, но необязательным при обновлении.

Для таких случаев используется динамическая генерация структур:

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

function UserStruct(context) {
  const base = {
    name: string()
  }

  if (context.mode === 'create') {
    base.age = number()
  } else {
    base.age = optional(number())
  }

  return object(base)
}

Здесь структура не является константой — она формируется на основе входного контекста.


Использование refinement для контекстных правил

Более гибкий подход реализуется через refinement, позволяющий добавлять пользовательские условия проверки.

import { refine, string } from 'superstruct'

const Password = refine(string(), 'Password', (value, context) => {
  if (context.role !== 'admin') {
    return value.length >= 8
  }
  return value.length >= 4
})

В этом примере правило длины пароля зависит от роли пользователя. Администраторы проходят менее строгую проверку, тогда как остальные пользователи — более строгую.

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


Контекст внутри вложенных структур

Вложенные структуры также могут реагировать на внешний контекст. Это особенно важно при работе с сложными объектами, содержащими массивы или вложенные сущности.

import { object, array, string, refine } from 'superstruct'

const Item = refine(string(), 'Item', (value, context) => {
  if (context.locale === 'ru') {
    return value.length > 0
  }
  return value.length >= 3
})

const Order = object({
  items: array(Item)
})

Здесь каждое значение массива проверяется с учётом локали, переданной через контекст.


Разделение логики через фабрики структур

При усложнении логики становится неудобно использовать inline-условия. Более масштабируемый подход — фабрики структур, возвращающие разные схемы в зависимости от контекста.

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

export function createUserStruct(context) {
  const isAdmin = context.role === 'admin'

  return object({
    username: string(),
    email: string(),
    permissions: isAdmin
      ? array(string())
      : optional(array(string()))
  })
}

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


Условные ветвления внутри union

Superstruct поддерживает объединения структур через union, что позволяет выбирать схему в зависимости от значения или контекста.

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

const Admin = object({
  role: string(),
  accessLevel: number()
})

const Guest = object({
  role: string()
})

const User = union([Admin, Guest])

Контекстно-зависимая логика может быть добавлена поверх union, например через обёртку:

function UserStruct(context) {
  return context.isAdmin ? Admin : Guest
}

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


Контекст в кастомных структурах

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

import { Struct } from 'superstruct'

const PositiveNumber = new Struct({
  type: 'positive-number',
  schema: 'number',
  *refine(value, context) {
    if (context.strict) {
      return value > 0
    }
    return value >= 0
  }
})

Здесь поведение структуры напрямую зависит от параметра strict в контексте.


Динамическое отключение и включение правил

Контекст может использоваться для переключения уровня строгости валидации:

  • строгий режим (production)
  • мягкий режим (development)
  • частичная валидация (partial update)
const Email = refine(string(), 'Email', (value, context) => {
  if (context.mode === 'partial') {
    return value === undefined || value.includes('@')
  }

  return value.includes('@')
})

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


Композиция контекстных правил

Сложные системы валидации обычно строятся через композицию нескольких уровней:

  • базовые структуры (типизация)
  • бизнес-правила (refine)
  • контекстные модификаторы (role, mode, feature flags)
const BaseUser = object({
  name: string(),
  email: string()
})

const WithRoleRules = refine(BaseUser, 'User', (value, context) => {
  if (context.role === 'guest' && value.email.endsWith('@internal')) {
    return false
  }
  return true
})

Композиция позволяет изолировать уровни логики и управлять ими независимо.


Ошибки проектирования контекстной валидации

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

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

Чрезмерная зависимость от контекста может привести к тому, что структура данных становится непредсказуемой, а поведение валидации — трудно воспроизводимым.


Интеграция контекста в масштабируемые системы

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

function buildContext(request) {
  return {
    role: request.user.role,
    mode: request.method === 'POST' ? 'create' : 'update',
    locale: request.headers['accept-language']
  }
}

Далее этот объект передаётся во все слои валидации, обеспечивая единый источник условий.


Стабильность схем при изменяемом контексте

Одной из ключевых особенностей является необходимость предсказуемости. Несмотря на динамическую природу контекста, структура должна оставаться контролируемой.

Поэтому часто применяется стратегия:

  • контекст влияет только на правила, но не на форму данных радикально
  • структура меняется минимально между режимами
  • различия выражаются через optional/required и диапазоны значений

Такой подход снижает риск расхождений между клиентской и серверной логикой.