При проверке сложных структур данных ошибка редко бывает единственной. Объект может одновременно содержать:
Валидация «до первой ошибки» подходит только для примитивных сценариев. В реальных приложениях требуется собрать полный набор проблем за один проход.
В библиотеке Superstruct механизм множественных ошибок строится вокруг:
StructError;failures();Базовая проверка через assert() выбрасывает исключение
при первой найденной ошибке.
import { object, string, number, assert } fr om 'superstruct'
const User = object({
name: string(),
age: number(),
})
assert({
name: 42,
age: '18',
}, User)
Ошибка:
Expected a string, but received: 42
На первый взгляд кажется, что библиотека завершает проверку
немедленно. Однако внутри StructError уже хранится
информация обо всех найденных нарушениях.
Ключевой механизм множественной обработки —
error.failures().
import {
object,
string,
number,
assert,
} fr om 'superstruct'
const User = object({
name: string(),
age: number(),
})
try {
assert({
name: 42,
age: '18',
}, User)
} catch (error) {
console.log([...error.failures()])
}
Результат:
[
{
value: 42,
type: 'string',
path: ['name'],
branch: [ ... ],
key: 'name',
message: 'Expected a string, but received: 42'
},
{
value: '18',
type: 'number',
path: ['age'],
branch: [ ... ],
key: 'age',
message: 'Expected a number, but received: "18"'
}
]
Каждый элемент failures() содержит детализированное
описание проблемы.
Некорректное значение.
{
value: '18'
}
Ожидаемый тип.
{
type: 'number'
}
Путь к проблемному полю.
{
path: ['profile', 'contacts', 0, 'email']
}
Особенно важно при работе с вложенными объектами и массивами.
Последний сегмент пути.
{
key: 'email'
}
Полный путь объектов от корня.
{
branch: [
rootObject,
profileObject,
contactsArray,
emailObject
]
}
Полезно для сложной диагностики и построения контекстных сообщений.
Готовое текстовое описание.
{
message: 'Expected a string, but received: 123'
}
import {
object,
string,
number,
boolean,
array,
assert,
} from 'superstruct'
const Product = object({
title: string(),
price: number(),
active: boolean(),
tags: array(string()),
})
try {
assert({
title: 100,
price: '500',
active: 'yes',
tags: [1, 2, 3],
}, Product)
} catch (error) {
for (const failure of error.failures()) {
console.log(failure.path, failure.message)
}
}
Результат:
['title'] Expected a string, but received: 100
['price'] Expected a number, but received: "500"
['active'] Expected a boolean, but received: "yes"
['tags', 0] Expected a string, but received: 1
['tags', 1] Expected a string, but received: 2
['tags', 2] Expected a string, but received: 3
В больших приложениях ошибки редко используются напрямую. Обычно создаётся единый формат.
function formatErrors(error) {
const result = {}
for (const failure of error.failures()) {
const path = failure.path.join('.')
result[path] = failure.message
}
return result
}
Использование:
try {
assert(data, Product)
} catch (error) {
console.log(formatErrors(error))
}
Результат:
{
'title': 'Expected a string, but received: 100',
'price': 'Expected a number, but received: "500"',
'tags.0': 'Expected a string, but received: 1'
}
Иногда одному полю соответствует несколько ограничений.
function collectErrors(error) {
const map = {}
for (const failure of error.failures()) {
const path = failure.path.join('.')
if (!map[path]) {
map[path] = []
}
map[path].push(failure.message)
}
return map
}
Результат:
{
email: [
'Invalid email format',
'Email is required'
]
}
import {
array,
string,
assert,
} from 'superstruct'
const Tags = array(string())
try {
assert([
'javascript',
100,
true,
], Tags)
} catch (error) {
console.log([...error.failures()])
}
Результат:
[
{
path: [1],
value: 100
},
{
path: [2],
value: true
}
]
Для UI важно точно понимать позицию элемента.
function mapArrayErrors(error) {
return [...error.failures()].map(failure => ({
index: failure.path[0],
message: failure.message,
}))
}
Результат:
[
{
index: 1,
message: 'Expected a string, but received: 100'
}
]
import {
object,
string,
number,
array,
assert,
} from 'superstruct'
const Address = object({
city: string(),
zip: number(),
})
const User = object({
name: string(),
addresses: array(Address),
})
try {
assert({
name: 100,
addresses: [
{
city: 123,
zip: '0001',
}
]
}, User)
} catch (error) {
for (const failure of error.failures()) {
console.log(failure.path)
}
}
Результат:
['name']
['addresses', 0, 'city']
['addresses', 0, 'zip']
Для сложных форм плоский список неудобен.
function buildErrorTree(error) {
const tree = {}
for (const failure of error.failures()) {
let current = tree
for (let i = 0; i < failure.path.length; i++) {
const segment = failure.path[i]
if (i === failure.path.length - 1) {
current[segment] = failure.message
} else {
current[segment] ||= {}
current = current[segment]
}
}
}
return tree
}
Результат:
{
addresses: {
0: {
city: 'Expected a string...',
zip: 'Expected a number...'
}
}
}
import {
string,
refine,
} from 'superstruct'
const Email = refine(
string(),
'email',
value => {
return value.includes('@')
}
)
Ошибка:
Expected a value of type `email`
const Password = refine(
refine(
string(),
'min_length',
value => value.length >= 8
),
'uppercase',
value => /[A-Z]/.test(value)
)
Проблема:
'abc'
Может вызвать несколько ошибок одновременно.
import {
define,
} from 'superstruct'
const PositiveNumber = define(
'PositiveNumber',
value => {
if (typeof value !== 'number') {
return 'Value must be a number'
}
if (value <= 0) {
return 'Value must be positive'
}
return true
}
)
Стандартный define() возвращает одну ошибку. Для сложной
логики применяется разбиение проверки.
const PasswordLength = refine(
string(),
'PasswordLength',
value => value.length >= 8
)
const PasswordUppercase = refine(
string(),
'PasswordUppercase',
value => /[A-Z]/.test(value)
)
const PasswordNumber = refine(
string(),
'PasswordNumber',
value => /\d/.test(value)
)
Композиция:
const Password = object({
password: PasswordLength,
})
Либо ручная стратегия:
function validatePassword(value) {
const errors = []
if (value.length < 8) {
errors.push('Minimum length is 8')
}
if (!/[A-Z]/.test(value)) {
errors.push('Uppercase letter required')
}
if (!/\d/.test(value)) {
errors.push('Number required')
}
return errors
}
Иногда необходимо сохранить валидные поля даже при наличии ошибок.
import {
validate,
} from 'superstruct'
const [error, value] = validate(data, User)
Если ошибки есть:
console.log(error)
Но при этом:
console.log(value)
может содержать частично корректную структуру.
Для высоконагруженных сценариев иногда выгоднее прекращать проверку после первой ошибки.
try {
assert(data, User)
} catch (error) {
console.log(error.message)
}
Подход уменьшает:
Часто применяется комбинированный подход:
| Сценарий | Стратегия |
|---|---|
| API | Полная агрегация |
| Streaming | Fail-fast |
| Формы | Полная агрегация |
| Внутренние сервисы | Частичная агрегация |
| CLI | Первая ошибка |
function validationMiddleware(struct) {
return (req, res, next) => {
const [error] = validate(req.body, struct)
if (!error) {
return next()
}
return res.status(400).json({
errors: [...error.failures()].map(failure => ({
field: failure.path.join('.'),
message: failure.message,
}))
})
}
}
Ответ:
{
"errors": [
{
"field": "email",
"message": "Expected a string"
},
{
"field": "age",
"message": "Expected a number"
}
]
}
function toFormErrors(error) {
return [...error.failures()].reduce((acc, failure) => {
acc[failure.path.join('.')] = failure.message
return acc
}, {})
}
const messages = {
string: 'Ожидается строка',
number: 'Ожидается число',
boolean: 'Ожидается булево значение',
}
Использование:
function translateFailure(failure) {
return {
field: failure.path.join('.'),
message: messages[failure.type],
}
}
Не все ошибки относятся к валидации.
import { StructError } from 'superstruct'
try {
assert(data, User)
} catch (error) {
if (error instanceof StructError) {
console.log([...error.failures()])
} else {
throw error
}
}
Полная агрегация ошибок требует:
Особенно дорого обходятся:
refine()-проверки;function takeFailures(error, lim it = 10) {
return [...error.failures()].slice(0, lim it)
}
function validateItems(items) {
for (const item of items) {
const [error] = validate(item, ItemStruct)
if (error) {
return error
}
}
}
function parseStructError(error) {
if (!(error instanceof StructError)) {
return null
}
return [...error.failures()].map(failure => ({
path: failure.path,
field: failure.path.join('.'),
type: failure.type,
message: failure.message,
value: failure.value,
}))
}
{
'user.name': 'Required',
'user.email': 'Invalid email'
}
Подходит для:
{
user: {
profile: {
email: 'Invalid'
}
}
}
Подходит для:
[
{
path: ['user', 'email'],
message: 'Invalid'
}
]
Подходит для:
| Подход | Преимущества | Недостатки |
|---|---|---|
| Fail-fast | Максимальная скорость | Мало информации |
| Полная агрегация | Удобство UI | Более высокая нагрузка |
| Ограниченная агрегация | Баланс производительности | Дополнительная логика |
| Древовидные ошибки | Удобство вложенных структур | Более сложная обработка |
| Плоские ошибки | Простая интеграция | Потеря контекста |
import {
validate,
StructError,
} from 'superstruct'
export function validateData(data, struct) {
const [error, value] = validate(data, struct)
if (!error) {
return {
success: true,
data: value,
errors: [],
}
}
if (!(error instanceof StructError)) {
throw error
}
return {
success: false,
data: null,
errors: [...error.failures()].map(failure => ({
field: failure.path.join('.'),
path: failure.path,
type: failure.type,
value: failure.value,
message: failure.message,
})),
}
}
Пример результата:
{
success: false,
data: null,
errors: [
{
field: 'email',
path: ['email'],
type: 'string',
value: 100,
message: 'Expected a string, but received: 100'
}
]
}