Динамические требования

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

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

  • if — определяет условие
  • then — применяется, если условие истинно
  • else — применяется, если условие ложно

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

const schema = {
  type: "object",
  properties: {
    type: { enum: ["user", "admin"] },
    permissions: { type: "array", items: { type: "string" } }
  },
  if: {
    properties: { type: { const: "admin" } }
  },
  then: {
    required: ["permissions"]
  },
  else: {
    not: {
      required: ["permissions"]
    }
  }
};

Здесь наличие поля permissions зависит от значения type. Такая схема позволяет описывать бизнес-логику прямо на уровне структуры данных.

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

Зависимые ключи: dependentRequired и dependentSchemas

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

dependentRequired

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

const schema = {
  type: "object",
  properties: {
    creditCard: { type: "string" },
    billingAddress: { type: "string" }
  },
  dependentRequired: {
    creditCard: ["billingAddress"]
  }
};

Если присутствует creditCard, то обязательно должно быть и billingAddress.

dependentSchemas

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

const schema = {
  type: "object",
  properties: {
    country: { type: "string" }
  },
  dependentSchemas: {
    country: {
      properties: {
        zipCode: { type: "string" }
      },
      required: ["zipCode"]
    }
  }
};

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

Динамическая проверка значений через $data

Ajv поддерживает расширение $data, которое позволяет ссылаться на значения других полей внутри схемы. Это открывает возможность создания зависимостей не только на уровне структуры, но и на уровне значений.

Для активации используется опция:

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

Пример:

const schema = {
  type: "object",
  properties: {
    min: { type: "number" },
    max: {
      type: "number",
      minimum: { $data: "1/min" }
    }
  }
};

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

Комбинирование allOf, anyOf и oneOf в динамических сценариях

Логические операторы JSON Schema часто используются для построения условных моделей данных.

allOf

Объединяет несколько схем, каждая из которых должна быть валидна.

const schema = {
  allOf: [
    {
      properties: {
        role: { const: "user" }
      }
    },
    {
      properties: {
        active: { type: "boolean" }
      }
    }
  ]
};

anyOf

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

const schema = {
  anyOf: [
    { required: ["email"] },
    { required: ["phone"] }
  ]
};

oneOf

Строгая альтернатива — должен соответствовать только одному варианту.

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

Динамические схемы через фабрики

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

const createSchema = (role) => {
  return {
    type: "object",
    properties: {
      id: { type: "string" },
      role: { const: role }
    },
    required: ["id", "role"]
  };
};

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

Пользовательские ключи (custom keywords)

Одним из наиболее мощных механизмов Ajv является возможность создания собственных ключей через addKeyword. Это позволяет реализовывать сложную динамическую логику, недоступную в стандартной JSON Schema.

ajv.addKeyword({
  keyword: "isPositiveIf",
  type: "number",
  schemaType: "boolean",
  compile: (schema) => {
    return function (data, dataPath, parentData) {
      if (schema && parentData.flag) {
        return data > 0;
      }
      return true;
    };
  }
});

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

const schema = {
  type: "object",
  properties: {
    value: { isPositiveIf: true },
    flag: { type: "boolean" }
  }
};

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

Macro и генерация схем

Ajv поддерживает макросы, которые трансформируют пользовательские ключи в стандартные схемы JSON Schema на этапе компиляции.

ajv.addKeyword({
  keyword: "range",
  macro: (schema) => {
    return {
      minimum: schema[0],
      maximum: schema[1]
    };
  }
});

Это позволяет расширять язык схем без потери производительности, так как итоговая структура остаётся валидной JSON Schema.

Динамика через контекст выполнения

В некоторых случаях требуется учитывать внешние параметры, не входящие в сам объект данных. Это реализуется через параметры функции validate или через фабрики схем.

const validate = ajv.compile({
  type: "object",
  properties: {
    limit: { type: "number", maximum: 100 }
  }
});

const result = validate(data);

Для динамических ограничений схема может генерироваться на основе окружения:

const schema = {
  type: "object",
  properties: {
    quota: { type: "number", maximum: config.maxQuota }
  }
};

Условные ограничения на уровне массивов

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

const schema = {
  type: "array",
  items: {
    type: "object",
    properties: {
      type: { enum: ["A", "B"] }
    },
    if: {
      properties: { type: { const: "A" } }
    },
    then: {
      required: ["valueA"]
    },
    else: {
      required: ["valueB"]
    }
  }
};

Каждый элемент массива проходит собственную ветвящуюся логику.

Строгий режим и влияние на динамические схемы

Ajv в строгом режиме (strict: true) накладывает ограничения на использование невалидных или неоднозначных конструкций. Это особенно важно для динамических схем, где часто возникают:

  • неявные типы
  • лишние поля
  • конфликтующие ключи

Строгий режим позволяет выявлять ошибки на этапе компиляции схемы, а не во время выполнения.

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

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

Пример типичного результата:

  • путь к объекту
  • тип условия (if/then/dependentRequired)
  • конкретное нарушенное ограничение

Это важно для построения интерфейсов, где требуется точная диагностика ошибок формы.

Паттерны построения динамических схем

На практике выделяются несколько устойчивых подходов:

  • Декларативные условия через if/then/else
  • Зависимости ключей через dependentRequired
  • Схемы-фабрики для конфигурационных систем
  • Пользовательские ключи для бизнес-логики
  • $data-ссылки для межполейных зависимостей

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

Производительность динамических проверок

Несмотря на выразительность, динамические конструкции могут влиять на производительность при неправильном использовании. Основные факторы:

  • избыточная глубина условий
  • частая генерация схем
  • использование сложных custom keywords с runtime-логикой

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

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

Сложные системы часто строятся через композицию:

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

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