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

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

Superstruct представляет собой инструмент для описания схем данных и их последующей проверки. Основная идея заключается в декларативном описании структуры объекта и строгой валидации входящих значений. Это особенно важно при обработке ответов API, где контроль над источником данных отсутствует.


Любой ответ API может быть описан через структуру (struct), которая определяет допустимые типы и форму данных. Например, ответ пользователя может выглядеть так:

{
  "id": 42,
  "name": "Alex",
  "email": "alex@example.com"
}

Для такой структуры в Superstruct создаётся схема:

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

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

После этого входящий ответ можно проверять:

import { create } from 'superstruct'

const data = await fetch('/api/user').then(r => r.json())

const user = create(data, User)

Функция create выполняет валидацию и возвращает либо корректно типизированный объект, либо выбрасывает ошибку при несоответствии структуры.


Поведение при ошибках валидации

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

Пример некорректного ответа:

{
  "id": "42",
  "name": "Alex"
}

Здесь поле id имеет строковый тип вместо числового, а email отсутствует.

Валидация выдаст структурированную ошибку, содержащую:

  • путь к полю (path)
  • ожидаемый тип
  • фактическое значение
import { validate } from 'superstruct'

const [error] = validate(data, User)

Функция validate возвращает кортеж, где первый элемент — ошибка, второй — результат проверки.


Обработка необязательных полей

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

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

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

Теперь отсутствие email не приводит к ошибке, но если поле присутствует, оно должно соответствовать типу string.


Вложенные структуры ответов

Часто API возвращают сложные объекты с вложенными структурами:

{
  "id": 1,
  "profile": {
    "nickname": "alex",
    "age": 30
  }
}

Superstruct позволяет описывать вложенные схемы через композицию:

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

const Profile = object({
  nickname: string(),
  age: number(),
})

const User = object({
  id: number(),
  profile: Profile,
})

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


Массивы в ответах API

API часто возвращают списки сущностей:

[
  { "id": 1, "name": "A" },
  { "id": 2, "name": "B" }
]

Для валидации массивов используется array:

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

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

const Items = array(Item)

Проверка гарантирует, что каждый элемент массива соответствует заданной структуре.


Преобразование и приведение типов

Некоторые API возвращают данные в «неудобном» формате, например числа в виде строк:

{
  "id": "10"
}

Superstruct позволяет использовать преобразование через coerce:

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

const NumberFromString = coerce(number(), string(), (value) =>
  Number(value)
)

Далее эта структура может использоваться внутри объектов:

const User = object({
  id: NumberFromString,
})

Это снижает необходимость ручного преобразования данных после получения ответа.


Уточнение правил через refine

Некоторые ограничения невозможно выразить только типами. Например, проверка диапазона значений:

import { refine, number } from 'superstruct'

const PositiveNumber = refine(number(), 'PositiveNumber', (value) => {
  return value > 0
})

Использование в структуре:

const Product = object({
  price: PositiveNumber,
})

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


Объединение возможных форматов ответа

API иногда возвращает разные структуры в зависимости от состояния:

{ "status": "ok", "data": {...} }

или

{ "status": "error", "message": "Not found" }

Для таких случаев используется union:

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

const Success = object({
  status: string(),
  data: object(),
})

const Error = object({
  status: string(),
  message: string(),
})

const Response = union([Success, Error])

Это позволяет корректно обрабатывать разные сценарии ответа без ручных проверок.


Защита слоя интеграции с API

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

Типичный поток обработки:

  1. Получение JSON из API
  2. Валидация через Superstruct
  3. Передача строго типизированного объекта в бизнес-логику
const response = await fetch('/api/product').then(r => r.json())

const product = create(response, Product)

processProduct(product)

Любое несоответствие структуры блокирует дальнейшую обработку данных на раннем этапе.


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

Ошибки валидации могут быть преобразованы в удобный формат для логирования или отображения:

import { StructError } from 'superstruct'

try {
  const data = create(response, User)
} catch (e) {
  if (e instanceof StructError) {
    console.log(e.path, e.message)
  }
}

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


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

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

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

const AdminUser = object({
  ...BaseUser.schema,
  role: string(),
})

Такой подход снижает дублирование и упрощает поддержку API-контрактов.


Стратегии валидации нестабильных API

При работе с внешними сервисами, структура ответов которых может изменяться, часто используется более мягкая валидация:

  • optional для нестабильных полей
  • default для значений по умолчанию
  • union для альтернативных форматов
  • coerce для приведения типов

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


Изоляция внешних данных

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

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