Валидация данных в 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()))
})
}
Такой подход позволяет централизовать всю контекстную логику в одном месте и избежать разрозненных условий по проекту.
unionSuperstruct поддерживает объединения структур через
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 в контексте.
Контекст может использоваться для переключения уровня строгости валидации:
const Email = refine(string(), 'Email', (value, context) => {
if (context.mode === 'partial') {
return value === undefined || value.includes('@')
}
return value.includes('@')
})
Таким образом, одна и та же структура адаптируется под разные сценарии использования без дублирования кода.
Сложные системы валидации обычно строятся через композицию нескольких уровней:
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']
}
}
Далее этот объект передаётся во все слои валидации, обеспечивая единый источник условий.
Одной из ключевых особенностей является необходимость предсказуемости. Несмотря на динамическую природу контекста, структура должна оставаться контролируемой.
Поэтому часто применяется стратегия:
Такой подход снижает риск расхождений между клиентской и серверной логикой.