Rename и переименование ключей

Метод rename() в библиотеке Joi предназначен для переименования полей объекта до выполнения основной валидации. Это особенно полезно при:

  • поддержке устаревших API;
  • нормализации входных данных;
  • миграции между версиями интерфейсов;
  • обработке разных вариантов именования (snake_case, camelCase);
  • объединении нескольких внешних источников данных;
  • создании совместимости между frontend и backend.

Базовый синтаксис

schema.rename(from, to, [options])

Аргументы

Аргумент Описание
from Исходное имя поля
to Новое имя поля
options Дополнительные параметры поведения

Простое переименование

const Joi = require('joi');

const schema = Joi.object({
    username: Joi.string().required()
}).rename('user_name', 'username');

const data = {
    user_name: 'alex'
};

const result = schema.validate(data);

console.log(result.value);

Результат:

{
    username: 'alex'
}

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


Что происходит внутри

Переименование выполняется в несколько этапов:

  1. Входящий объект анализируется.
  2. Joi ищет поле from.
  3. Значение переносится в to.
  4. Старое поле удаляется.
  5. Запускается обычная валидация схемы.

Это означает, что схема никогда не увидит исходное имя поля.


Переименование нескольких ключей

const schema = Joi.object({
    firstName: Joi.string(),
    lastName: Joi.string()
})
.rename('first_name', 'firstName')
.rename('last_name', 'lastName');

Входные данные:

{
    first_name: 'John',
    last_name: 'Doe'
}

После обработки:

{
    firstName: 'John',
    lastName: 'Doe'
}

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

Метод rename() поддерживает объект настроек:

.rename(from, to, {
    alias,
    multiple,
    override,
    ignoreUndefined
})

Параметр alias

Поведение по умолчанию

Без alias старое поле удаляется.

const schema = Joi.object({
    login: Joi.string()
}).rename('username', 'login');

Вход:

{
    username: 'admin'
}

Результат:

{
    login: 'admin'
}

Поле username исчезает.


Сохранение исходного ключа

const schema = Joi.object({
    login: Joi.string()
}).rename('username', 'login', {
    alias: true
});

Результат:

{
    username: 'admin',
    login: 'admin'
}

Теперь создаётся копия вместо переноса.


Практическое применение alias

Поддержка старых клиентов API

const schema = Joi.object({
    email: Joi.string().email()
}).rename('mail', 'email', {
    alias: true
});

Старые клиенты используют mail, новые — email.


Параметр multiple

Ошибка при конфликте

По умолчанию Joi запрещает нескольким полям переименовываться в один ключ.

const schema = Joi.object({
    name: Joi.string()
})
.rename('username', 'name')
.rename('login', 'name');

Вход:

{
    username: 'alex',
    login: 'root'
}

Ошибка:

Error: Cannot rename "login" because multiple renames are disabled

Разрешение нескольких переименований

const schema = Joi.object({
    name: Joi.string()
})
.rename('username', 'name', {
    multiple: true
})
.rename('login', 'name', {
    multiple: true
});

Какое значение победит

Последнее переименование перезаписывает предыдущее.

{
    username: 'alex',
    login: 'root'
}

Результат:

{
    name: 'root'
}

Параметр override

Защита существующего поля

Если целевой ключ уже существует, Joi выбрасывает ошибку.

const schema = Joi.object({
    name: Joi.string()
}).rename('username', 'name');

Вход:

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

Ошибка:

Error: Cannot rename "username" because override is disabled

Принудительное перезаписывание

const schema = Joi.object({
    name: Joi.string()
}).rename('username', 'name', {
    override: true
});

Результат:

{
    name: 'alex'
}

Старое значение name будет уничтожено.


Параметр ignoreUndefined

Стандартное поведение

Если поле отсутствует, Joi может выбрасывать ошибку в определённых сценариях.

.rename('oldField', 'newField')

Игнорирование отсутствующего поля

.rename('oldField', 'newField', {
    ignoreUndefined: true
})

Теперь отсутствие oldField не вызывает проблем.


Переименование перед валидацией

Это одна из важнейших особенностей rename().

const schema = Joi.object({
    age: Joi.number().integer().min(18)
}).rename('user_age', 'age');

Вход:

{
    user_age: 25
}

Сначала выполняется:

{
    age: 25
}

И только затем запускается:

Joi.number().integer().min(18)

Работа с вложенными объектами

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

const schema = Joi.object({
    profile: Joi.object({
        firstName: Joi.string()
    }).rename('first_name', 'firstName')
});

Входные данные

{
    profile: {
        first_name: 'Alex'
    }
}

Результат:

{
    profile: {
        firstName: 'Alex'
    }
}

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

rename() поддерживает переименование через RegExp.


Массовое переименование

const schema = Joi.object()
    .rename(/^(\w+)_id$/, Joi.ex * pression('{#1}Id'));

Как это работает

Вход:

{
    user_id: 1,
    product_id: 20
}

Результат:

{
    userId: 1,
    productId: 20
}

Joi.ex * pression()

Метод Joi.ex * pression() позволяет использовать шаблоны при генерации нового имени.

Joi.ex * pression('{#1}Id')

#1 — первая группа регулярного выражения.


Более сложный пример

const schema = Joi.object()
    .rename(
        /^api_(\w+)_(\w+)$/,
        Joi.ex * pression('{#1}.{#2}')
    );

Вход:

{
    api_user_name: 'Alex'
}

Результат:

{
    'user.name': 'Alex'
}

Переименование snake_case → camelCase

Одна из самых популярных задач.


Ручной вариант

const schema = Joi.object({
    firstName: Joi.string(),
    lastName: Joi.string()
})
.rename('first_name', 'firstName')
.rename('last_name', 'lastName');

Автоматизация через RegExp

const schema = Joi.object()
    .rename(
        /^(\w+)_(\w+)$/,
        Joi.ex * pression('{#1}{#2}')
    );

Однако такой вариант не преобразует буквы в верхний регистр.


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

Для настоящего camelCase обычно используют предварительную обработку данных перед Joi.


Комбинирование с unknown()

Часто rename() применяется вместе с разрешением неизвестных ключей.

const schema = Joi.object({
    username: Joi.string()
})
.rename('user_name', 'username')
.unknown(true);

Порядок выполнения операций

Порядок методов имеет значение.

Joi.object()
    .rename('a', 'b')
    .rename('b', 'c');

Вход:

{
    a: 1
}

Результат:

{
    c: 1
}

Переименование выполняется последовательно.


Цепочки переименований

Каскадное преобразование

const schema = Joi.object()
    .rename('fname', 'first_name')
    .rename('first_name', 'firstName');

Результат:

{
    firstName: 'John'
}

Ошибки при использовании rename()

Конфликт ключей

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

При переименовании username -> name возникает конфликт.


Потеря данных

override: true

может уничтожить существующие значения.


Неочевидные цепочки

.rename('a', 'b')
.rename('b', 'c')
.rename('c', 'd')

Сложные каскады ухудшают читаемость схемы.


Практический пример: поддержка нескольких API

Старый формат

{
    user_name: 'alex',
    user_mail: 'alex@example.com'
}

Новый формат

{
    username: 'alex',
    email: 'alex@example.com'
}

Универсальная схема

const schema = Joi.object({
    username: Joi.string().required(),
    email: Joi.string().email().required()
})
.rename('user_name', 'username')
.rename('user_mail', 'email');

Практический пример: миграция версии API

Версия 1

{
    fullname: 'John Doe'
}

Версия 2

{
    fullName: 'John Doe'
}

Совместимая схема

const schema = Joi.object({
    fullName: Joi.string()
})
.rename('fullname', 'fullName', {
    alias: true
});

Использование вместе с strip()

Иногда требуется переименовать поле и удалить старые данные.

const schema = Joi.object({
    password: Joi.string(),
    oldPassword: Joi.string().strip()
})
.rename('old_password', 'oldPassword');

Использование с when()

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

const schema = Joi.object({
    role: Joi.string(),
    permissions: Joi.array().when('role', {
        is: 'admin',
        then: Joi.required()
    })
})
.rename('user_role', 'role');

Использование с prefs()

const schema = Joi.object({
    username: Joi.string()
})
.rename('user_name', 'username')
.prefs({
    abortEarly: false
});

Переименование никак не влияет на глобальные настройки схемы.


Производительность

rename() работает быстро, однако большое количество переименований может:

  • усложнять поддержку схемы;
  • увеличивать стоимость обработки объектов;
  • ухудшать читаемость;
  • создавать трудноуловимые конфликты.

Рекомендации

Использовать rename() когда:

  • требуется совместимость между версиями API;
  • необходимо унифицировать данные;
  • поддерживаются legacy-форматы;
  • внешние сервисы используют разные соглашения об именовании.

Избегать rename() когда:

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

Полный пример

const Joi = require('joi');

const schema = Joi.object({
    firstName: Joi.string().required(),
    lastName: Joi.string().required(),
    age: Joi.number().integer().min(18)
})
.rename('first_name', 'firstName')
.rename('last_name', 'lastName')
.rename('user_age', 'age');

const data = {
    first_name: 'Alex',
    last_name: 'Doe',
    user_age: 25
};

const result = schema.validate(data);

console.log(result.value);

Результат:

{
    firstName: 'Alex',
    lastName: 'Doe',
    age: 25
}