allOf для пересечения

Ключевое слово allOf в JSON Schema и библиотеке Ajv используется для композиции схем через операцию логического «И». Объект считается валидным только в том случае, если он соответствует всем схемам, перечисленным в массиве allOf. Это позволяет строить сложные валидационные правила из небольших переиспользуемых блоков.


Семантика allOf

Конструкция allOf задаётся как массив схем:

{
  "allOf": [
    { "type": "object", "required": ["id"] },
    { "type": "object", "required": ["name"] }
  ]
}

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

Формально:

  • S = {S₁, S₂, …, Sₙ}
  • объект валиден ⇔ ∀ Sᵢ ∈ S: validate(Sᵢ, data) = true

Механизм обработки в Ajv

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

  1. входной объект передаётся в первую схему
  2. затем последовательно проверяется всеми остальными схемами
  3. ошибки агрегируются в единый список
  4. результат считается успешным только при отсутствии ошибок во всех схемах

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


Базовый пример использования

import Ajv from "ajv";

const ajv = new Ajv();

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

const validate = ajv.compile(schema);

console.log(validate({ id: 1, name: "Item" })); // true
console.log(validate({ id: 1 })); // false

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


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

allOf часто используется для декомпозиции схем на логические блоки.

Базовая сущность

{
  "type": "object",
  "properties": {
    "createdAt": { "type": "string", "format": "date-time" }
  },
  "required": ["createdAt"]
}

Расширение сущности

{
  "allOf": [
    {
      "type": "object",
      "properties": {
        "id": { "type": "number" }
      },
      "required": ["id"]
    },
    {
      "type": "object",
      "properties": {
        "createdAt": { "type": "string", "format": "date-time" }
      },
      "required": ["createdAt"]
    }
  ]
}

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


Наследование через allOf

В экосистеме JSON Schema allOf часто используется как механизм псевдо-наследования.

Базовая схема

{
  "type": "object",
  "properties": {
    "type": { "type": "string" }
  },
  "required": ["type"]
}

Производная схема

{
  "allOf": [
    {
      "$ref": "#/definitions/base"
    },
    {
      "type": "object",
      "properties": {
        "type": { "const": "user" },
        "email": { "type": "string" }
      },
      "required": ["email"]
    }
  ]
}

Здесь первая часть задаёт общий контракт, вторая — специализацию.


Взаимодействие allOf с $ref

Ajv активно оптимизирует схемы, содержащие ссылки:

{
  "allOf": [
    { "$ref": "#/definitions/a" },
    { "$ref": "#/definitions/b" }
  ]
}

Во время компиляции:

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

Это уменьшает накладные расходы при больших схемах.


Ошибки валидации и их структура

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

Пример:

const schema = {
  allOf: [
    {
      type: "object",
      required: ["id"],
      properties: {
        id: { type: "number" }
      }
    },
    {
      type: "object",
      required: ["name"],
      properties: {
        name: { type: "string" }
      }
    }
  ]
};

const validate = ajv.compile(schema);

validate({});

console.log(validate.errors);

Результат содержит:

  • отсутствие id
  • отсутствие name

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


Вложенные allOf

Конструкция может быть вложенной:

{
  "allOf": [
    {
      "allOf": [
        { "type": "object" },
        { "required": ["id"] }
      ]
    },
    {
      "properties": {
        "id": { "type": "number" }
      }
    }
  ]
}

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


Совместное использование с другими комбинирующими операторами

allOf часто используется вместе с:

  • anyOf
  • oneOf
  • not

Пример комбинации

{
  "allOf": [
    {
      "type": "object"
    },
    {
      "anyOf": [
        { "required": ["email"] },
        { "required": ["phone"] }
      ]
    }
  ]
}

Логика:

  • объект должен быть объектом
  • дополнительно обязан содержать либо email, либо phone

Типичные сценарии применения

1. Сборка DTO-моделей

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

  • идентификаторы
  • метаданные
  • бизнес-атрибуты

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

{
  "allOf": [
    { "$ref": "v1.json" },
    { "$ref": "v2.json" }
  ]
}

Позволяет расширять контракт без разрушения обратной совместимости.


3. Декларативные ограничения

Комбинирование базового типа и уточняющих правил:

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

Производительность в Ajv

В Ajv allOf обрабатывается эффективно за счёт:

  • предварительной компиляции схем
  • кеширования функций валидации
  • устранения дублирующихся проверок
  • оптимизации порядка выполнения (fail-fast поведение)

Если первая схема уже заведомо провалилась, последующие проверки могут не выполняться в зависимости от конфигурации strict mode и параметров обработки ошибок.


Особенности поведения additionalProperties

При использовании allOf важно учитывать взаимодействие с additionalProperties.

{
  "allOf": [
    {
      "properties": { "id": { "type": "number" } }
    },
    {
      "properties": { "name": { "type": "string" } },
      "additionalProperties": false
    }
  ]
}

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


Частые ошибки проектирования

Дублирование ограничений

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

Несогласованность required

{
  "allOf": [
    { "required": ["id"] },
    { "required": ["id"] }
  ]
}

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

Пересечение incompatible constraints

{
  "allOf": [
    { "type": "string" },
    { "type": "number" }
  ]
}

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


Роль allOf в архитектуре схем

allOf выступает как базовый строительный механизм композиции:

  • объединение ограничений
  • формирование сложных контрактов
  • поддержка модульной структуры схем
  • реализация наследования на уровне JSON Schema

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