Преобразование ключей: camelCase и snakeCase

Роль преобразования ключей в схемах валидации

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

Библиотека Yup ориентирована на валидацию значений, а не на преобразование структуры объекта, однако механизм трансформаций позволяет внедрять преобразование ключей на уровне схемы.

Ключевая особенность: Yup не предоставляет встроенных методов camelCase или snake_case для ключей, поэтому преобразование реализуется через transform, кастомные методы и предварительную нормализацию данных.


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

Различие форматов ключей возникает на стыке систем:

  • REST API: user_name, created_at
  • Frontend (JavaScript): userName, createdAt
  • Базы данных: чаще snake_case
  • GraphQL-клиенты: преимущественно camelCase

Несогласованность приводит к необходимости унификации данных до этапа валидации или внутри неё.


Подходы к преобразованию ключей в Yup

1. Предварительная нормализация данных

Наиболее стабильный и предсказуемый подход — преобразование объекта до передачи в Yup.

Используются утилиты:

  • camelcase-keys
  • snakecase-keys

Пример логики:

import camelcaseKeys from "camelcase-keys";

const normalizedData = camelcaseKeys(inputData, { deep: true });

schema.validateSync(normalizedData);

Особенности подхода:

  • Yup получает уже согласованную структуру
  • отсутствует риск нарушения схемы
  • трансформация не зависит от логики валидации

2. Использование transform на уровне объекта

Yup предоставляет метод transform, который применяется до проверки значений.

Базовый принцип: преобразование входного объекта целиком перед началом валидации.

import * as yup from "yup";
import camelcaseKeys from "camelcase-keys";

const schema = yup.object({
  userName: yup.string(),
  createdAt: yup.date()
}).transform((value, originalValue) => {
  return camelcaseKeys(originalValue, { deep: true });
});

В этом случае:

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

3. Рекурсивное преобразование ключей

При отсутствии внешних библиотек применяется ручная рекурсия.

CamelCase преобразование

const toCamelCase = (str) =>
  str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());

const camelizeObject = (obj) => {
  if (Array.isArray(obj)) {
    return obj.map(camelizeObject);
  }

  if (obj !== null && typeof obj === "object") {
    return Object.keys(obj).reduce((acc, key) => {
      const newKey = toCamelCase(key);
      acc[newKey] = camelizeObject(obj[key]);
      return acc;
    }, {});
  }

  return obj;
};

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

const schema = yup.object().transform((value, originalValue) => {
  return camelizeObject(originalValue);
});

4. Преобразование camelCase → snake_case

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

const toSnakeCase = (str) =>
  str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);

const snakeizeObject = (obj) => {
  if (Array.isArray(obj)) {
    return obj.map(snakeizeObject);
  }

  if (obj !== null && typeof obj === "object") {
    return Object.keys(obj).reduce((acc, key) => {
      const newKey = toSnakeCase(key);
      acc[newKey] = snakeizeObject(obj[key]);
      return acc;
    }, {});
  }

  return obj;
};

Поведение transform в Yup

Метод transform имеет два ключевых параметра:

  • value — текущее значение после предыдущих трансформаций
  • originalValue — исходные данные до валидации

Важно учитывать:

  • transform выполняется до cast
  • порядок трансформаций влияет на итоговый результат
  • при работе с объектами следует использовать stripUnknown аккуратно

Вложенные объекты

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

Пример структуры:

{
  user_profile: {
    first_name: "John",
    last_name: "Doe"
  }
}

После camelCase:

{
  userProfile: {
    firstName: "John",
    lastName: "Doe"
  }
}

Yup-схема:

const schema = yup.object({
  userProfile: yup.object({
    firstName: yup.string(),
    lastName: yup.string()
  })
}).transform((value, originalValue) => camelizeObject(originalValue));

Работа с массивами объектов

Массивы требуют отдельной обработки, так как содержат вложенные структуры.

const schema = yup.object({
  users: yup.array().of(
    yup.object({
      userName: yup.string(),
      createdAt: yup.date()
    })
  )
}).transform((value, originalValue) => camelizeObject(originalValue));

Рекурсивная функция должна учитывать:

  • массивы на любом уровне вложенности
  • объекты внутри массивов
  • смешанные структуры

Кастомные методы Yup

Для переиспользования логики преобразования создаются расширения через addMethod.

import * as yup from "yup";

yup.addMethod(yup.object, "camelizeKeys", function () {
  return this.transform((value, originalValue) => {
    return camelizeObject(originalValue);
  });
});

const schema = yup.object().camelizeKeys();

Такой подход позволяет:

  • унифицировать трансформации
  • избегать дублирования кода
  • централизовать поведение схем

Типичные ошибки при преобразовании ключей

Потеря синхронизации схемы и данных

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

Двойное преобразование

Повторное применение transform или внешней нормализации приводит к некорректным ключам:

  • user_nameuserNameuserName (повторное преобразование может исказить данные)

Конфликт stripUnknown

При включённом stripUnknown: true могут удаляться ключи, которые были корректно преобразованы, но отсутствуют в схеме.


Производственные аспекты

Преобразование ключей на уровне Yup увеличивает нагрузку на валидацию:

  • рекурсивные операции имеют сложность O(n)
  • большие объекты с глубокими структурами могут замедлять обработку

Оптимизация:

  • предварительная нормализация предпочтительнее
  • использование мемоизации для часто повторяющихся структур
  • минимизация глубины вложенности

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

Подход Преимущества Недостатки
Pre-transform (до Yup) высокая предсказуемость дополнительный слой обработки
transform в Yup централизованная логика сложнее отладка
рекурсивная функция гибкость риск ошибок и нагрузка
кастомные методы переиспользование усложнение архитектуры

Архитектурные рекомендации

  • единый стиль ключей должен задаваться на уровне API-контракта
  • Yup использовать как слой валидации, а не трансформации данных
  • преобразование ключей целесообразно выносить в отдельный сервисный слой
  • при смешанных системах предпочтительна стратегия адаптеров данных

Особенности взаимодействия с TypeScript

При использовании TypeScript изменение ключей влияет на типизацию:

  • требуется отдельное описание типов для camelCase и snake_case
  • автоматическая трансформация типов отсутствует
  • часто применяется утилита zod-to-ts или ручные интерфейсы

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