Версионирование схем в 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()
})
);
Это полезно при добавлении новых версий, основанных на предыдущих.
Одной из наиболее сложных задач версионирования является изменение типа поля.
Например:
age — строка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 в этом контексте используется как финальный слой проверки уже приведённых данных.
Особую сложность представляет версионирование вложенных объектов:
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;Каждая стратегия выбирается исходя из масштаба системы и частоты изменений структуры данных.