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

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

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

function isString(value) {
  return typeof value === 'string';
}

function isNumber(value) {
  return typeof value === 'number' && !Number.isNaN(value);
}

function isBoolean(value) {
  return typeof value === 'boolean';
}

Для массивов используется Array.isArray, поскольку typeof [] возвращает object.

function isArray(value) {
  return Array.isArray(value);
}

Для объектов необходима дополнительная защита от null:

function isObject(value) {
  return value !== null && typeof value === 'object' && !Array.isArray(value);
}

Валидация структуры объектов

При отсутствии схемы в формате JSON Schema структура описывается через набор проверок полей.

function validateUser(data) {
  if (!isObject(data)) {
    return { valid: false, error: 'Ожидается объект' };
  }

  if (!isString(data.name)) {
    return { valid: false, error: 'Поле name должно быть строкой' };
  }

  if (!isNumber(data.age)) {
    return { valid: false, error: 'Поле age должно быть числом' };
  }

  return { valid: true };
}

Такой подход масштабируется плохо при росте модели данных, но остаётся предсказуемым и прозрачным.

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

Отсутствие библиотеки требует явного контроля наличия ключей через hasOwnProperty или оператор in.

function hasKey(obj, key) {
  return Object.prototype.hasOwnProperty.call(obj, key);
}

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

if (!hasKey(data, 'email')) {
  return { valid: false, error: 'Отсутствует email' };
}

Опциональные поля обрабатываются условно:

if (hasKey(data, 'phone') && !isString(data.phone)) {
  return { valid: false, error: 'phone должен быть строкой' };
}

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

При работе с вложенными объектами проверка становится рекурсивной.

function validateAddress(address) {
  if (!isObject(address)) {
    return { valid: false, error: 'address должен быть объектом' };
  }

  if (!isString(address.city)) {
    return { valid: false, error: 'city обязателен' };
  }

  return { valid: true };
}

function validateUser(data) {
  if (!isObject(data)) {
    return { valid: false, error: 'Некорректный объект' };
  }

  const addressResult = validateAddress(data.address);
  if (!addressResult.valid) {
    return addressResult;
  }

  return { valid: true };
}

Подобная композиция функций заменяет вложенные схемы.

Проверка массивов объектов

Массивы требуют итерации по каждому элементу.

function validateUsers(list) {
  if (!isArray(list)) {
    return { valid: false, error: 'Ожидается массив' };
  }

  for (let i = 0; i < list.length; i++) {
    const result = validateUser(list[i]);
    if (!result.valid) {
      return { valid: false, error: `Элемент ${i}: ${result.error}` };
    }
  }

  return { valid: true };
}

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

Реализация ограниченного JSON Schema-подобного механизма

Часто создаётся собственный мини-язык описания структуры.

const schema = {
  type: 'object',
  required: ['name', 'age'],
  properties: {
    name: { type: 'string' },
    age: { type: 'number' }
  }
};

Базовый валидатор:

function validateSchema(data, schema) {
  if (schema.type === 'object') {
    if (!isObject(data)) {
      return { valid: false, error: 'Не объект' };
    }

    if (schema.required) {
      for (const key of schema.required) {
        if (!(key in data)) {
          return { valid: false, error: `Нет поля ${key}` };
        }
      }
    }

    if (schema.properties) {
      for (const key in schema.properties) {
        if (key in data) {
          const propSchema = schema.properties[key];
          const value = data[key];

          const type = propSchema.type;
          if (type === 'string' && !isString(value)) {
            return { valid: false, error: `${key} должен быть строкой` };
          }

          if (type === 'number' && !isNumber(value)) {
            return { valid: false, error: `${key} должен быть числом` };
          }
        }
      }
    }
  }

  return { valid: true };
}

Такой подход фактически воспроизводит ограниченную часть логики JSON Schema без внешних зависимостей.

Обработка дополнительных свойств

В системах без готового валидатора часто требуется вручную контролировать «лишние» поля.

function validateNoExtras(data, allowedKeys) {
  for (const key in data) {
    if (!allowedKeys.includes(key)) {
      return { valid: false, error: `Лишнее поле ${key}` };
    }
  }
  return { valid: true };
}

Интеграция с общей схемой:

const allowed = ['name', 'age', 'address'];

const extraCheck = validateNoExtras(data, allowed);
if (!extraCheck.valid) {
  return extraCheck;
}

Система накопления ошибок

Вместо возврата первой ошибки может использоваться сбор всех нарушений.

function validateUserCollect(data) {
  const errors = [];

  if (!isObject(data)) {
    errors.push('Ожидается объект');
    return { valid: false, errors };
  }

  if (!isString(data.name)) {
    errors.push('name должно быть строкой');
  }

  if (!isNumber(data.age)) {
    errors.push('age должно быть числом');
  }

  return {
    valid: errors.length === 0,
    errors
  };
}

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

Контроль глубокой вложенности

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

function safeValidate(value, seen = new Set()) {
  if (value && typeof value === 'object') {
    if (seen.has(value)) {
      return { valid: false, error: 'Циклическая структура' };
    }
    seen.add(value);
  }

  return { valid: true };
}

Упрощённый типизированный контракт

На практике часто вводится декларативное описание:

const userContract = {
  name: 'string',
  age: 'number',
  active: 'boolean'
};

И универсальный проверяющий слой:

function validateContract(data, contract) {
  for (const key in contract) {
    const expectedType = contract[key];
    const value = data[key];

    if (expectedType === 'string' && !isString(value)) {
      return { valid: false, error: key };
    }

    if (expectedType === 'number' && !isNumber(value)) {
      return { valid: false, error: key };
    }

    if (expectedType === 'boolean' && !isBoolean(value)) {
      return { valid: false, error: key };
    }
  }

  return { valid: true };
}

Ограничения подхода без специализированной библиотеки

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

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