Валидация пользовательского ввода

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

Библиотека Ajv реализует проверку данных на основе спецификации JSON Schema и считается одной из самых быстрых и функциональных реализаций в экосистеме JavaScript. Её использование особенно актуально в системах, где требуется строгая типизация входных данных при сохранении динамичности языка.

В основе Ajv лежит идея декларативного описания структуры данных. Вместо написания процедурных проверок используется схема, описывающая допустимые значения.

Пример простой схемы:

const schema = {
  type: "object",
  properties: {
    username: { type: "string" },
    age: { type: "integer", minimum: 0 }
  },
  required: ["username", "age"],
  additionalProperties: false
};

Такая схема описывает объект, который обязан содержать строковое поле username и целочисленное поле age, причём возраст не может быть отрицательным.

Базовая валидация данных

Создание экземпляра валидатора и проверка данных:

import Ajv from "ajv";

const ajv = new Ajv();

const validate = ajv.compile(schema);

const data = {
  username: "user1",
  age: 25
};

const valid = validate(data);

if (!valid) {
  console.log(validate.errors);
}

Метод compile преобразует схему в оптимизированную функцию проверки. Это важная особенность Ajv: схема компилируется один раз, после чего валидация выполняется максимально быстро.

Структура схем и типизация

Ajv строго опирается на типы JSON Schema:

  • string — строки
  • number — числа с плавающей точкой
  • integer — целые числа
  • boolean — логические значения
  • object — объекты
  • array — массивы
  • null — пустое значение

Пример более сложной типизации:

const schema = {
  type: "object",
  properties: {
    id: { type: "integer" },
    email: { type: "string", format: "email" },
    isActive: { type: "boolean" }
  },
  required: ["id", "email"]
};

Проверка обязательных и дополнительных полей

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

required: ["id", "email"]

Если поле отсутствует, валидация завершится ошибкой.

Запрет дополнительных свойств:

additionalProperties: false

Это особенно важно для API, где необходимо жёстко контролировать входящий контракт данных.

Работа с массивами

Ajv позволяет детально описывать структуру массивов:

const schema = {
  type: "object",
  properties: {
    tags: {
      type: "array",
      items: { type: "string" },
      minItems: 1,
      uniqueItems: true
    }
  }
};

Здесь массив tags должен содержать только строки, минимум один элемент и не допускать дубликатов.

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

Одно из наиболее распространённых применений — валидация вложенных объектов:

const schema = {
  type: "object",
  properties: {
    user: {
      type: "object",
      properties: {
        name: { type: "string" },
        profile: {
          type: "object",
          properties: {
            age: { type: "integer" },
            city: { type: "string" }
          },
          required: ["age"]
        }
      },
      required: ["name", "profile"]
    }
  }
};

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

Форматы данных

Ajv поддерживает проверку форматов:

  • email
  • uri
  • date-time
  • ipv4 / ipv6

Пример:

email: { type: "string", format: "email" }

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

Преобразование типов (coercion)

Ajv может автоматически приводить типы:

const ajv = new Ajv({ coerceTypes: true });

Пример: строка "25" может быть преобразована в число 25. Это полезно при обработке данных из HTTP-запросов, где всё приходит в виде строк.

Строгий режим

Строгая проверка помогает выявлять ошибки в схемах:

const ajv = new Ajv({ strict: true });

В строгом режиме библиотека предупреждает о:

  • неопределённых свойствах схемы
  • некорректных ключах
  • потенциально ошибочных конструкциях

Пользовательские ошибки и диагностика

Ajv возвращает массив ошибок:

if (!valid) {
  console.log(validate.errors);
}

Структура ошибки:

  • instancePath — путь к ошибочному полю
  • message — описание ошибки
  • keyword — тип нарушения

Пример обработки:

validate.errors.forEach(err => {
  console.log(`${err.instancePath} ${err.message}`);
});

Пользовательские ключевые слова

Ajv позволяет расширять систему валидации:

ajv.addKeyword({
  keyword: "isEven",
  validate: function (schema, data) {
    return typeof data === "number" && data % 2 === 0;
  }
});

Использование:

const schema = {
  type: "object",
  properties: {
    value: { isEven: true }
  }
};

Асинхронная валидация

Ajv поддерживает асинхронные проверки, например обращение к базе данных:

const schema = {
  type: "object",
  properties: {
    username: {
      type: "string",
      async: true,
      validate: async (value) => {
        const exists = await checkUser(value);
        return !exists;
      }
    }
  }
};

Для этого требуется использование compileAsync.

Композиция схем

Схемы можно переиспользовать:

const addressSchema = {
  type: "object",
  properties: {
    city: { type: "string" },
    zip: { type: "string" }
  }
};

const userSchema = {
  type: "object",
  properties: {
    name: { type: "string" },
    address: addressSchema
  }
};

Это снижает дублирование и улучшает поддержку кода.

Условная валидация

Ajv поддерживает сложные условия:

const schema = {
  type: "object",
  properties: {
    role: { type: "string" }
  },
  if: {
    properties: { role: { const: "admin" } }
  },
  then: {
    required: ["permissions"]
  }
};

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

Работа с ошибками в API

В серверной разработке результат Ajv обычно преобразуется в стандартизированный формат:

const errors = validate.errors.map(err => ({
  field: err.instancePath,
  message: err.message
}));

Это позволяет возвращать клиенту структурированный ответ:

{
  "errors": [
    {
      "field": "/age",
      "message": "should be >= 0"
    }
  ]
}

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

Ajv оптимизирован для высоконагруженных систем:

  • компиляция схемы один раз
  • минимизация runtime-проверок
  • кеширование валидаторов

Рекомендуется:

  • избегать динамического создания схем в циклах
  • переиспользовать compiled validators
  • включать строгие режимы только при разработке

Безопасность входных данных

Использование схемы позволяет:

  • ограничить неожиданные поля
  • предотвратить инъекции лишних данных
  • контролировать формат входа API

Особенно важно сочетание:

additionalProperties: false

и строгих типов, что формирует предсказуемый контракт данных.

Использование в реальных приложениях

Типичные сценарии применения:

  • валидация body в REST API
  • проверка query-параметров
  • контроль данных форм
  • валидация сообщений очередей
  • проверка конфигураций сервисов