Валидация структур данных в 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 };
}
Такая стратегия позволяет локализовать ошибку на уровне элемента коллекции.
Часто создаётся собственный мини-язык описания структуры.
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 };
}
Ручная валидация приводит к дублированию логики, увеличению поверхности ошибок и сложностям поддержки при росте доменной модели. Отсутствие декларативной схемы делает сложные структуры менее наглядными, а расширение правил требует модификации кода вместо изменения описания данных.
При этом сохраняется полный контроль над исполнением, отсутствует зависимость от внешних пакетов и достигается предсказуемость поведения в рантайме, что особенно важно в минималистичных или встроенных средах исполнения.