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

Преобразование типов в Joi является одной из ключевых возможностей, позволяющих не только проверять входные данные, но и приводить их к ожидаемому формату ещё до передачи в бизнес-логику приложения. Это особенно важно в серверных приложениях на Node.js, где данные часто приходят из HTTP-запросов в виде строк, даже если по смыслу они должны быть числами, булевыми значениями или структурированными объектами.

Joi по умолчанию работает в режиме, при котором входные значения проходят через этап преобразования (casting) перед валидацией. Это означает, что библиотека пытается привести данные к типу, указанному в схеме, прежде чем применять правила проверки.

Например, строка "42" может быть автоматически преобразована в число 42, если схема ожидает тип number.

Базовый принцип:

  • вход → преобразование → валидация → результат

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

Автоматическое преобразование и опция convert

Ключевая настройка, отвечающая за преобразование типов, — convert.

schema.validate(value, { convert: true })

По умолчанию convert: true, то есть Joi активно пытается привести данные к нужному типу.

Пример:

import Joi from 'joi';

const schema = Joi.object({
  age: Joi.number().integer().min(0)
});

const result = schema.validate({ age: "25" });

console.log(result.value);

Результат:

{ age: 25 }

Здесь строка "25" была автоматически преобразована в число 25.

Если отключить преобразование:

const result = schema.validate({ age: "25" }, { convert: false });

console.log(result.value);

Результат:

{ age: "25" }

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

Влияние преобразования на разные типы

Числа

Joi активно преобразует строки в числа:

const schema = Joi.object({
  price: Joi.number()
});

schema.validate({ price: "19.99" });

Результат:

{ price: 19.99 }

Однако преобразование имеет ограничения:

  • "10abc" → ошибка
  • "" → может стать NaN или вызвать ошибку в зависимости от правил
  • null → не преобразуется автоматически в 0

Булевы значения

Преобразование в boolean особенно важно, поскольку HTTP часто передаёт значения как строки:

const schema = Joi.object({
  isActive: Joi.boolean()
});

Примеры преобразований:

  • "true"true
  • "false"false
  • "1"true
  • "0"false
schema.validate({ isActive: "1" });
// { isActive: true }

Важно учитывать, что не все строки интерпретируются как boolean. Например, "yes" или "no" не всегда преобразуются автоматически.

Строки

Строки в Joi обычно не требуют преобразования, но библиотека может приводить другие типы к строкам:

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

schema.validate({ id: 123 });

Результат:

{ id: "123" }

Это поведение может быть полезным при работе с идентификаторами, приходящими из базы данных или URL.

Явные методы преобразования

Joi предоставляет ряд методов, которые явно управляют трансформацией данных.

number(), string(), boolean() как кастинг-инструменты

Каждый базовый тип уже включает встроенное преобразование:

Joi.number()
Joi.string()
Joi.boolean()

Они не только валидируют, но и приводят тип.

default()

Метод default() не является прямым преобразованием типа, но участвует в нормализации данных:

const schema = Joi.object({
  role: Joi.string().default('user')
});

schema.validate({});

Результат:

{ role: "user" }

custom() для ручного преобразования

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

const schema = Joi.object({
  value: Joi.string().custom((value, helpers) => {
    return value.trim().toLowerCase();
  })
});

Пример:

schema.validate({ value: "  HELLO  " });

Результат:

{ value: "hello" }

Трансформация строк

Строковые методы часто используются как часть преобразования данных:

Joi.string().trim().lowercase().uppercase()

Хотя lowercase() и uppercase() взаимно исключают смысловое использование, они демонстрируют идею цепочки трансформаций.

Пример:

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

schema.validate({ username: "  JohnDoe " });

Результат:

{ username: "johndoe" }

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

Объекты

Joi может нормализовать структуру объектов, удаляя лишние поля:

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

Вход:

{ name: "Alex", age: 30 }

Результат:

{ name: "Alex" }

Здесь происходит не только валидация, но и структурное преобразование.

Массивы

Массивы также подвергаются преобразованию элементов:

const schema = Joi.array().items(Joi.number());

Вход:

["1", "2", "3"]

Результат:

[1, 2, 3]

Каждый элемент преобразуется индивидуально.

Глубокое преобразование вложенных структур

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

const schema = Joi.object({
  user: Joi.object({
    age: Joi.number(),
    active: Joi.boolean()
  })
});

Вход:

{
  user: {
    age: "20",
    active: "true"
  }
}

Результат:

{
  user: {
    age: 20,
    active: true
  }
}

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

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

Важно понимать порядок операций:

  1. Применение кастинга
  2. Применение default()
  3. Проверка правил (min, max, pattern)
  4. Возврат результата

Это влияет на поведение сложных схем. Например:

const schema = Joi.number().min(10);

schema.validate("15");

Сначала "15" превращается в 15, затем проверяется условие min(10).

Отключение преобразования на уровне схемы

Можно полностью запретить преобразование:

const schema = Joi.number().strict();

Теперь:

schema.validate("42");

Результат — ошибка, так как строка не будет преобразована.

Глобальное управление преобразованием

Для сложных приложений удобно задавать единое поведение:

const schema = Joi.object({
  id: Joi.number()
}).prefs({ convert: false });

Теперь вся схема работает без автоматического кастинга.

Практическое значение преобразования типов

Преобразование типов валидационной библиотекой решает несколько прикладных задач:

  • устранение проблем HTTP-типизации (всё приходит строками)
  • снижение необходимости ручного парсинга parseInt, JSON.parse
  • унификация входных данных
  • предотвращение ошибок бизнес-логики из-за неожиданных типов

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

Типичные ошибки при работе с преобразованием

  1. Неожиданное приведение пустых строк

    Joi.number().validate("")

    Может привести к NaN или ошибке.

  2. Потеря точности при преобразовании

    • строки с десятичными числами иногда округляются в зависимости от настроек
  3. Скрытые преобразования boolean

    • "false" может интерпретироваться неожиданно в некоторых конфигурациях
  4. Автоматическое удаление полей

    • неизвестные свойства исчезают при .unknown(false)

Контроль предсказуемости данных

В сложных системах часто комбинируют подходы:

  • convert: true на уровне API-слоя
  • strict() на уровне доменной логики
  • явные .custom() для нестандартных случаев

Это позволяет разделить зоны ответственности:

  • API слой: нормализация
  • бизнес слой: строгая типизация
  • инфраструктура: минимальные преобразования

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