Unknown, forbidden и strip

По умолчанию Joi строго относится к структуре объекта. Если схема описывает только определённые поля, любые дополнительные ключи считаются ошибкой.

const Joi = require('joi');

const schema = Joi.object({
  username: Joi.string().required(),
  age: Joi.number()
});

const result = schema.validate({
  username: 'alex',
  age: 25,
  role: 'admin'
});

console.log(result.error.message);

Результат:

"role" is not allowed

Поле role отсутствует в схеме, поэтому валидация завершается ошибкой.


Метод .unknown()

Метод .unknown() разрешает наличие дополнительных полей, которые не описаны в схеме.

const schema = Joi.object({
  username: Joi.string().required()
}).unknown();

const result = schema.validate({
  username: 'alex',
  role: 'admin',
  permissions: ['read', 'write']
});

console.log(result.error);

Результат:

undefined

Все лишние ключи сохраняются в объекте и не вызывают ошибку.


Ограничение неизвестных ключей

.unknown(false) явно запрещает неизвестные поля.

const schema = Joi.object({
  username: Joi.string()
}).unknown(false);

const result = schema.validate({
  username: 'alex',
  role: 'admin'
});

console.log(result.error.message);

Результат:

"role" is not allowed

Фактически это поведение используется по умолчанию.


Глобальная настройка allowUnknown

Вместо вызова .unknown() можно использовать опцию allowUnknown.

const schema = Joi.object({
  username: Joi.string()
});

const result = schema.validate(
  {
    username: 'alex',
    role: 'admin'
  },
  {
    allowUnknown: true
  }
);

console.log(result.value);

Результат:

{
  username: 'alex',
  role: 'admin'
}

Разница между .unknown() и allowUnknown

.unknown()

Изменяет саму схему.

const schema = Joi.object({
  name: Joi.string()
}).unknown();

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


allowUnknown

Изменяет поведение конкретной операции валидации.

schema.validate(data, {
  allowUnknown: true
});

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


Поведение во вложенных объектах

Разрешение неизвестных ключей не распространяется автоматически на дочерние объекты.

const schema = Joi.object({
  profile: Joi.object({
    name: Joi.string()
  })
}).unknown();

const result = schema.validate({
  profile: {
    name: 'Alex',
    city: 'Berlin'
  }
});

console.log(result.error.message);

Результат:

"profile.city" is not allowed

Внешний объект допускает дополнительные поля, но вложенный объект profile остаётся строгим.


Разрешение неизвестных ключей во вложенной схеме

const schema = Joi.object({
  profile: Joi.object({
    name: Joi.string()
  }).unknown()
});

const result = schema.validate({
  profile: {
    name: 'Alex',
    city: 'Berlin'
  }
});

console.log(result.error);

Результат:

undefined

Метод .forbidden()

Полное запрещение поля

.forbidden() запрещает присутствие ключа в объекте.

const schema = Joi.object({
  username: Joi.string(),
  password: Joi.string(),
  isAdmin: Joi.forbidden()
});

const result = schema.validate({
  username: 'alex',
  password: '123456',
  isAdmin: true
});

console.log(result.error.message);

Результат:

"isAdmin" is not allowed

Защита серверных полей

.forbidden() особенно полезен для блокировки полей, которые не должны приходить от клиента.

const createUserSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),

  id: Joi.forbidden(),
  createdAt: Joi.forbidden(),
  updatedAt: Joi.forbidden(),
  role: Joi.forbidden()
});

Такая схема защищает серверную логику от попыток подмены данных.


Отличие от отсутствия поля в схеме

Без .forbidden() поведение зависит от настроек unknown-ключей.

Без .forbidden()

const schema = Joi.object({
  username: Joi.string()
}).unknown();
schema.validate({
  username: 'alex',
  role: 'admin'
});

Поле role будет разрешено.


С .forbidden()

const schema = Joi.object({
  username: Joi.string(),
  role: Joi.forbidden()
}).unknown();
schema.validate({
  username: 'alex',
  role: 'admin'
});

Теперь поле запрещено даже при разрешённых unknown-ключах.


Условительное запрещение

.forbidden() часто используется вместе с when().

const schema = Joi.object({
  type: Joi.string().valid('user', 'guest').required(),

  password: Joi.when('type', {
    is: 'guest',
    then: Joi.forbidden(),
    otherwise: Joi.string().required()
  })
});

Проверка guest

const result = schema.validate({
  type: 'guest',
  password: '123456'
});

console.log(result.error.message);

Результат:

"password" is not allowed

Проверка user

const result = schema.validate({
  type: 'user',
  password: '123456'
});

console.log(result.error);

Результат:

undefined

Запрещённые поля и PATCH-запросы

При обновлении данных некоторые поля обычно запрещены к изменению.

const updateSchema = Joi.object({
  email: Joi.string().email(),
  username: Joi.string(),

  id: Joi.forbidden(),
  role: Joi.forbidden(),
  balance: Joi.forbidden()
});

Метод .strip()

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

.strip() удаляет поле из результирующего объекта.

const schema = Joi.object({
  username: Joi.string(),
  password: Joi.string().strip()
});

const result = schema.validate({
  username: 'alex',
  password: '123456'
});

console.log(result.value);

Результат:

{
  username: 'alex'
}

Поле прошло валидацию, но было удалено из результата.


Разница между .strip() и .forbidden()

.forbidden()

Поле вообще не должно существовать.

Joi.string().forbidden()

.strip()

Поле допускается, но удаляется после проверки.

Joi.string().strip()

Практический пример

.forbidden()

const schema = Joi.object({
  role: Joi.forbidden()
});
schema.validate({
  role: 'admin'
});

Ошибка:

"role" is not allowed

.strip()

const schema = Joi.object({
  role: Joi.string().strip()
});
schema.validate({
  role: 'admin'
});

Результат:

{}

Очистка служебных данных

.strip() часто используется для удаления внутренних полей.

const schema = Joi.object({
  username: Joi.string(),
  password: Joi.string(),
  internalToken: Joi.string().strip(),
  debugInfo: Joi.any().strip()
});

Удаление подтверждения пароля

Типичный сценарий регистрации пользователя.

const schema = Joi.object({
  password: Joi.string().min(8).required(),

  confirmPassword: Joi.string()
    .valid(Joi.ref('password'))
    .required()
    .strip()
});

Проверка

const result = schema.validate({
  password: 'secret123',
  confirmPassword: 'secret123'
});

console.log(result.value);

Результат:

{
  password: 'secret123'
}

Использование .strip() во вложенных объектах

const schema = Joi.object({
  profile: Joi.object({
    username: Joi.string(),
    tempField: Joi.string().strip()
  })
});

const result = schema.validate({
  profile: {
    username: 'alex',
    tempField: 'temporary'
  }
});

console.log(result.value);

Результат:

{
  profile: {
    username: 'alex'
  }
}

Массовая очистка данных

.strip() помогает преобразовывать входные данные в безопасную структуру.

const schema = Joi.object({
  username: Joi.string().required(),
  email: Joi.string().email().required(),

  csrfToken: Joi.string().strip(),
  trackingId: Joi.string().strip(),
  analytics: Joi.any().strip()
});

После валидации остаются только полезные данные.


Совместное использование unknown, forbidden и strip

Комбинация правил

const schema = Joi.object({
  username: Joi.string().required(),

  role: Joi.forbidden(),

  tempData: Joi.any().strip()
}).unknown();

Проверка

const result = schema.validate({
  username: 'alex',
  tempData: {
    cache: true
  },
  extraField: 'allowed',
  role: 'admin'
});

console.log(result.error.message);

Результат:

"role" is not allowed

Поведение после удаления forbidden-поля

const result = schema.validate({
  username: 'alex',
  tempData: {
    cache: true
  },
  extraField: 'allowed'
});

console.log(result.value);

Результат:

{
  username: 'alex',
  extraField: 'allowed'
}

Поле tempData удалено, extraField сохранён благодаря .unknown().


Стратегии использования

Строгий API

Joi.object({
  username: Joi.string(),
  email: Joi.string().email()
})

Дополнительные поля запрещены.

Подходит для:

  • финансовых систем;
  • административных панелей;
  • критичных API;
  • внутренних сервисов.

Гибкий API

Joi.object({
  username: Joi.string()
}).unknown()

Дополнительные поля разрешены.

Подходит для:

  • интеграций;
  • webhook-обработчиков;
  • динамических JSON-структур;
  • промежуточных сервисов.

Безопасная очистка

Joi.object({
  password: Joi.string(),
  confirmPassword: Joi.string().strip(),
  role: Joi.forbidden()
})

Комбинация:

  • запрещает опасные поля;
  • удаляет временные данные;
  • сохраняет только полезную информацию.