Частичная валидация

Модель проверки фрагментов данных

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

В Ajv это достигается сочетанием возможностей компиляции схем, повторного использования валидаторов и композиции JSON Schema.


Поведение Ajv при неполных объектах

По умолчанию Ajv проверяет объект относительно схемы целиком. Если в схеме объявлено обязательное поле, а его нет в переданных данных, результат будет ошибочным:

const Ajv = require("ajv");
const ajv = new Ajv();

const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "number" }
  },
  required: ["name", "age"],
  additionalProperties: false
};

const validate = ajv.compile(schema);

validate({ name: "Ivan" }); // false
console.log(validate.errors);

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


Ослабление обязательности полей

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

Условное снятие required

const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "number" }
  },
  additionalProperties: false
};

Теперь объект { name: "Ivan" } считается валидным, так как отсутствует блок required.

Такой подход превращает схему в «описание формы данных», а не строгий контракт.


Частичная валидация через динамические схемы

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

const nameSchema = {
  type: "object",
  properties: {
    name: { type: "string", minLength: 2 }
  },
  required: ["name"],
  additionalProperties: false
};

const ageSchema = {
  type: "object",
  properties: {
    age: { type: "number", minimum: 0 }
  },
  required: ["age"],
  additionalProperties: false
};

Каждый фрагмент компилируется отдельно:

const validateName = ajv.compile(nameSchema);
const validateAge = ajv.compile(ageSchema);

validateName({ name: "Ivan" }); // true
validateAge({ age: 30 });       // true

Этот подход используется при:

  • частичных обновлениях профиля пользователя
  • валидации PATCH-запросов
  • поэтапных формах

Валидация только изменённого фрагмента

При обновлении одного поля нет необходимости валидировать весь объект. Вместо этого применяется локальная проверка изменения.

const user = {
  name: "Ivan",
  age: 30
};

// обновление только age
const patch = { age: 31 };

validateAge(patch); // проверяется только изменённая часть
Object.assign(user, patch);

Такой подход снижает вычислительную нагрузку и упрощает контроль ошибок.


Использование $data и зависимых правил

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

const schema = {
  type: "object",
  properties: {
    role: { type: "string" },
    accessLevel: { type: "number" }
  },
  if: {
    properties: { role: { const: "admin" } }
  },
  then: {
    required: ["accessLevel"]
  }
};

Если передан только role, Ajv применяет условие без необходимости наличия остальных полей.

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


Частичная валидация через removeAdditional

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

const ajv = new Ajv({ removeAdditional: "all" });

Опция позволяет автоматически очищать объект от незадекларированных свойств.

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


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

Ajv поддерживает разбиение схем на модули с помощью $ref, что позволяет валидировать отдельные части структуры независимо.

const addressSchema = {
  $id: "address",
  type: "object",
  properties: {
    city: { type: "string" },
    street: { type: "string" }
  },
  required: ["city"]
};

const userSchema = {
  type: "object",
  properties: {
    name: { type: "string" },
    address: { $ref: "address" }
  }
};

Компиляция:

ajv.addSchema(addressSchema);

const validateUser = ajv.compile(userSchema);

Теперь можно отдельно валидировать только address:

const validateAddress = ajv.getSchema("address");
validateAddress({ city: "Almaty" }); // true

Такой подход формирует естественную модель частичной проверки вложенных структур.


Валидаторы для PATCH-операций

JSON Patch и Merge Patch часто требуют проверки только изменяемого фрагмента. В Ajv это реализуется через изолированные схемы операций.

Пример для PATCH:

const patchSchema = {
  type: "object",
  properties: {
    op: { enum: ["replace", "add", "remove"] },
    path: { type: "string" },
    value: {}
  },
  required: ["op", "path"]
};

Каждый элемент массива patch проверяется отдельно:

const validatePatch = ajv.compile(patchSchema);

validatePatch({ op: "replace", path: "/name", value: "Alex" });

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


Стратегия проверки вложенных изменений

В сложных структурах часто требуется проверять только конкретный уровень вложенности.

const schema = {
  type: "object",
  properties: {
    user: {
      type: "object",
      properties: {
        profile: {
          type: "object",
          properties: {
            name: { type: "string" }
          }
        }
      }
    }
  }
};

Вместо полной валидации всего объекта можно выделить фрагмент:

const profileSchema = schema.properties.user.properties.profile;
const validateProfile = ajv.compile(profileSchema);

validateProfile({ name: "Ivan" });

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


Частичная валидация и strict режим

В строгом режиме Ajv усиливает контроль схемы и выявляет несоответствия структуры ещё на этапе компиляции.

const ajv = new Ajv({ strict: true });

При частичной валидации это помогает обнаруживать:

  • неиспользуемые свойства
  • некорректные ссылки $ref
  • конфликтующие типы

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


Кеширование компилированных валидаторов

Частичная валидация часто приводит к множеству мелких схем. Повторная компиляция становится узким местом, поэтому используется кеширование:

const validators = new Map();

function getValidator(schemaKey, schema) {
  if (!validators.has(schemaKey)) {
    validators.set(schemaKey, ajv.compile(schema));
  }
  return validators.get(schemaKey);
}

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


Ошибки при частичной валидации

При проверке фрагментов данных важно учитывать особенности формирования ошибок:

  • ошибки относятся только к переданному фрагменту
  • отсутствующие поля не всегда считаются нарушением
  • контекст родительской схемы может быть недоступен
const validate = ajv.compile({
  type: "object",
  required: ["name"],
  properties: {
    name: { type: "string" }
  }
});

validate({}); // false, но причина локальна — отсутствует name

В частичной модели такие ошибки интерпретируются иначе: отсутствие поля может быть допустимым состоянием.


Изоляция валидации в сервисной архитектуре

В распределённых системах частичная валидация применяется на уровне сервисов:

  • сервис профиля проверяет только данные профиля
  • сервис авторизации проверяет только токены
  • сервис уведомлений валидирует только свои payload

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


Совмещение частичной и полной валидации

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

  • частичная валидация — на уровне API и UI
  • полная валидация — перед сохранением или публикацией
if (validatePatch(patch)) {
  Object.assign(entity, patch);
  validateFull(entity);
}

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