Версионирование схем

Версионирование схем в Yup

Версионирование схем в валидационных библиотеках становится критически важным в условиях, когда структура данных развивается во времени. При работе с Yup изменения валидационных правил неизбежно затрагивают совместимость между клиентом и сервером, между различными версиями API, а также между сохранёнными данными и текущими правилами проверки.

Основная сложность заключается в том, что схема Yup выступает не просто как набор проверок, а как формальный контракт структуры данных. Любое изменение этого контракта может привести к ошибкам валидации уже существующих объектов.


Каждая Yup-схема фактически описывает определённое состояние модели данных. Когда структура данных изменяется, возникает новая версия контракта. Это может проявляться в следующих сценариях:

  • добавление новых обязательных полей;
  • удаление ранее существовавших полей;
  • изменение типов данных (например, строка → число);
  • изменение логики проверки (min, max, matches, test);
  • переход от одного формата объекта к другому.

Yup не предоставляет встроенного механизма версионирования, поэтому управление версиями реализуется на уровне архитектуры приложения.


Хранение версии данных

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

const dataV1 = {
  version: 1,
  name: "Alex",
  age: "25"
};

const dataV2 = {
  version: 2,
  fullName: "Alex Johnson",
  age: 25
};

Поле version становится ключевым маркером выбора схемы валидации.


Разделение схем по версиям

На практике создаются отдельные Yup-схемы для каждой версии структуры данных.

import * as Yup from "yup";

const schemaV1 = Yup.object({
  version: Yup.number().oneOf([1]).required(),
  name: Yup.string().required(),
  age: Yup.string().required()
});

const schemaV2 = Yup.object({
  version: Yup.number().oneOf([2]).required(),
  fullName: Yup.string().required(),
  age: Yup.number().required()
});

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


Диспетчеризация схем по версии

Для выбора нужной схемы используется функция-диспетчер:

const schemas = {
  1: schemaV1,
  2: schemaV2
};

function validate(data) {
  const schema = schemas[data.version];
  return schema.validate(data);
}

Это обеспечивает явное соответствие версии и схемы, снижая риск некорректной валидации.


Совместимость и мягкая миграция

В реальных системах данные часто существуют в смешанных форматах. Полная жёсткая изоляция версий может быть неудобной, поэтому применяется стратегия обратной совместимости.

Yup позволяет использовать условные проверки через .when():

const schema = Yup.object({
  version: Yup.number().required(),

  name: Yup.string().when("version", {
    is: 1,
    then: (s) => s.required(),
    otherwise: (s) => s.notRequired()
  }),

  fullName: Yup.string().when("version", {
    is: 2,
    then: (s) => s.required(),
    otherwise: (s) => s.notRequired()
  })
});

Такой подход объединяет несколько версий в одну схему, но увеличивает сложность поддержки.


Трансформация данных перед валидацией

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

function migrate(data) {
  if (data.version === 1) {
    return {
      version: 2,
      fullName: data.name,
      age: Number(data.age)
    };
  }
  return data;
}

После миграции применяется единая актуальная схема:

const currentSchema = Yup.object({
  version: Yup.number().required(),
  fullName: Yup.string().required(),
  age: Yup.number().required()
});

Такое разделение снижает когнитивную нагрузку на схемы и делает логику эволюции данных явной.


Композиция схем и переиспользование

Yup позволяет строить схемы через композицию, что полезно при версионировании общих частей:

const baseV1 = {
  name: Yup.string().required()
};

const baseV2 = {
  fullName: Yup.string().required()
};

const schemaV1 = Yup.object({
  version: Yup.number().oneOf([1]),
  ...baseV1,
  age: Yup.string().required()
});

const schemaV2 = Yup.object({
  version: Yup.number().oneOf([2]),
  ...baseV2,
  age: Yup.number().required()
});

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


Инкрементальные изменения и .concat()

При расширении схем без нарушения предыдущих версий применяется метод .concat():

const baseSchema = Yup.object({
  name: Yup.string().required()
});

const extendedSchema = baseSchema.concat(
  Yup.object({
    age: Yup.number().required()
  })
);

Это полезно при добавлении новых версий, основанных на предыдущих.


Управление изменением типов данных

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

Например:

  • версия 1: age — строка
  • версия 2: age — число

В Yup это решается через предварительное приведение или кастомную проверку:

const schema = Yup.object({
  version: Yup.number().required(),
  age: Yup.mixed().test("age-type", function (value) {
    const { version } = this.parent;

    if (version === 1) {
      return typeof value === "string";
    }

    if (version === 2) {
      return typeof value === "number";
    }

    return false;
  })
});

Такой подход даёт гибкость, но снижает декларативность схемы.


Разделение логики валидации и бизнес-правил

С ростом количества версий схемы начинают смешивать валидацию структуры и бизнес-логику. Для устойчивой архитектуры эти уровни разделяются:

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

Деградация старых версий

В системах с длительным жизненным циклом старые версии схем постепенно теряют поддержку. Это выражается в:

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

Yup в этом контексте используется как финальный слой проверки уже приведённых данных.


Версионирование вложенных структур

Особую сложность представляет версионирование вложенных объектов:

const addressV1 = Yup.object({
  city: Yup.string().required()
});

const addressV2 = Yup.object({
  city: Yup.string().required(),
  zip: Yup.string().required()
});

const userSchemaV2 = Yup.object({
  version: Yup.number().oneOf([2]),
  address: addressV2
});

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


Стратегии устойчивого развития схем

На практике применяются несколько моделей:

  • Изолированные версии схем — строгая разделённость логики;
  • Условные схемы — единая схема с ветвлением через when;
  • Миграция данных — приведение всех входных данных к последней версии;
  • Композиционная модель — построение новых версий на базе старых;
  • Гибридный подход — сочетание миграции и версионных схем.

Каждая стратегия выбирается исходя из масштаба системы и частоты изменений структуры данных.