Конструкция if-then-else

В спецификации JSON Schema конструкция if-then-else используется для условной валидации данных. Библиотека Ajv поддерживает этот механизм начиная с черновика JSON Schema Draft-07.

Конструкция позволяет:

  • применять разные правила в зависимости от содержимого объекта;
  • реализовывать зависимые поля;
  • описывать бизнес-логику декларативно;
  • избегать громоздких комбинаций oneOf, anyOf и allOf.

Синтаксис напоминает условные операторы в языках программирования:

if (условие) {
  then(правила)
} else {
  else(другие_правила)
}

В JSON Schema это выглядит так:

{
  "if": {
    "properties": {
      "type": {
        "const": "admin"
      }
    }
  },
  "then": {
    "required": ["permissions"]
  },
  "else": {
    "required": ["guestToken"]
  }
}

Принцип работы if

Ключевое слово if описывает схему-условие.

Если данные проходят проверку по схеме внутри if, активируется then.

Если данные не проходят проверку по if, активируется else.

Важно понимать:

  • if сам по себе не влияет на успешность валидации;
  • if только определяет, какая ветка будет применяться;
  • ошибки появляются внутри then или else.

Базовый пример

Схема

const schema = {
  type: "object",

  properties: {
    age: { type: "number" },
    hasLicense: { type: "boolean" }
  },

  if: {
    properties: {
      age: {
        minimum: 18
      }
    }
  },

  then: {
    required: ["hasLicense"]
  }
}

Валидация

const Ajv = require("ajv")

const ajv = new Ajv()

const validate = ajv.compile(schema)

console.log(validate({
  age: 20,
  hasLicense: true
}))

Результат:

true

Проверка ошибки

console.log(validate({
  age: 20
}))

Результат:

false

Ошибки:

console.log(validate.errors)
[
  {
    instancePath: "",
    schemaPath: "#/then/required",
    keyword: "required",
    params: {
      missingProperty: "hasLicense"
    },
    message: "must have required property 'hasLicense'"
  }
]

Работа ветки else

Схема

const schema = {
  type: "object",

  properties: {
    role: { type: "string" },
    adminCode: { type: "string" },
    guestCode: { type: "string" }
  },

  if: {
    properties: {
      role: { const: "admin" }
    }
  },

  then: {
    required: ["adminCode"]
  },

  else: {
    required: ["guestCode"]
  }
}

Проверка администратора

validate({
  role: "admin",
  adminCode: "A-100"
})

Результат:

true

Ошибка администратора

validate({
  role: "admin"
})

Результат:

false

Поскольку условие if выполнилось, была активирована ветка then.


Проверка гостя

validate({
  role: "guest",
  guestCode: "G-200"
})

Результат:

true

Особенность работы properties

Распространённая ошибка связана с тем, что properties не требует обязательного наличия поля.

Пример:

if: {
  properties: {
    role: { const: "admin" }
  }
}

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

Почему это происходит

properties проверяет поле только если оно существует.

Следовательно:

{}

пройдёт условие if.


Правильный вариант

Для строгой проверки необходимо использовать required.

if: {
  required: ["role"],

  properties: {
    role: { const: "admin" }
  }
}

Теперь отсутствие role приведёт к переходу в ветку else.


Использование нескольких условий

Проверка типа оплаты

const schema = {
  type: "object",

  properties: {
    paymentType: {
      enum: ["card", "cash"]
    },

    cardNumber: {
      type: "string"
    }
  },

  if: {
    properties: {
      paymentType: {
        const: "card"
      }
    },

    required: ["paymentType"]
  },

  then: {
    required: ["cardNumber"]
  }
}

Сложные условия через allOf

if может содержать полноценную JSON Schema.

Пример

if: {
  allOf: [
    {
      properties: {
        country: {
          const: "USA"
        }
      }
    },

    {
      properties: {
        age: {
          minimum: 21
        }
      }
    }
  ]
}

Условие выполнится только если:

  • страна — USA;
  • возраст не меньше 21.

Комбинация с pattern

Валидация телефона

const schema = {
  type: "object",

  properties: {
    country: { type: "string" },
    phone: { type: "string" }
  },

  if: {
    properties: {
      country: {
        const: "RU"
      }
    },

    required: ["country"]
  },

  then: {
    properties: {
      phone: {
        pattern: "^\\+7"
      }
    }
  },

  else: {
    properties: {
      phone: {
        pattern: "^\\+1"
      }
    }
  }
}

Вложенные конструкции

if-then-else можно вкладывать друг в друга.

Пример тарифной системы

const schema = {
  type: "object",

  properties: {
    plan: { type: "string" },
    users: { type: "number" }
  },

  if: {
    properties: {
      plan: { const: "business" }
    }
  },

  then: {
    if: {
      properties: {
        users: {
          minimum: 100
        }
      }
    },

    then: {
      properties: {
        supportLevel: {
          const: "premium"
        }
      },

      required: ["supportLevel"]
    }
  }
}

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

Запрет определённых комбинаций

const schema = {
  type: "object",

  properties: {
    role: { type: "string" },
    active: { type: "boolean" }
  },

  if: {
    properties: {
      role: { const: "banned" }
    }
  },

  then: {
    properties: {
      active: {
        const: false
      }
    }
  }
}

Условная проверка массива

Пример

const schema = {
  type: "object",

  properties: {
    type: { type: "string" },
    items: {
      type: "array"
    }
  },

  if: {
    properties: {
      type: {
        const: "nonEmpty"
      }
    }
  },

  then: {
    properties: {
      items: {
        minItems: 1
      }
    }
  }
}

Условная валидация чисел

const schema = {
  type: "object",

  properties: {
    mode: { type: "string" },
    value: { type: "number" }
  },

  if: {
    properties: {
      mode: { const: "positive" }
    }
  },

  then: {
    properties: {
      value: {
        minimum: 1
      }
    }
  },

  else: {
    properties: {
      value: {
        maximum: -1
      }
    }
  }
}

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

const schema = {
  type: "object",

  properties: {
    notificationType: { type: "string" },
    contact: { type: "string" }
  },

  if: {
    properties: {
      notificationType: {
        const: "email"
      }
    }
  },

  then: {
    properties: {
      contact: {
        format: "email"
      }
    }
  },

  else: {
    properties: {
      contact: {
        pattern: "^\\+"
      }
    }
  }
}

Совместное использование с dependentSchemas

Пример

const schema = {
  type: "object",

  properties: {
    creditCard: {
      type: "string"
    },

    billingAddress: {
      type: "string"
    }
  },

  dependentSchemas: {
    creditCard: {
      if: {
        required: ["creditCard"]
      },

      then: {
        required: ["billingAddress"]
      }
    }
  }
}

Генерация кода в Ajv

Ajv компилирует схемы в JavaScript-функции.

Для if-then-else создаётся оптимизированный код с условными переходами.

Упрощённо это выглядит так:

if (validateIf(data)) {
  validateThen(data)
} else {
  validateElse(data)
}

Благодаря компиляции:

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

Особенности Draft-07

Ключевые слова:

  • if
  • then
  • else

были официально добавлены в JSON Schema Draft-07.

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

{
  $schema: "http://json-schema.org/draft-07/schema#"
}

Поведение при отсутствии then

Если присутствует только if, никаких ограничений не применяется.

Пример

{
  if: {
    properties: {
      type: {
        const: "admin"
      }
    }
  }
}

Такая схема бесполезна, потому что результат if нигде не используется.


Поведение при отсутствии else

Это наиболее распространённый вариант.

{
  if: { ... },
  then: { ... }
}

Если условие не выполнится:

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

Использование нескольких if

JSON Schema не поддерживает массив условий:

{
  if: [ ... ]
}

Неверно.

Правильный подход — комбинация через:

  • allOf
  • anyOf
  • oneOf

Пример

{
  allOf: [
    {
      if: { ... },
      then: { ... }
    },

    {
      if: { ... },
      then: { ... }
    }
  ]
}

Практический пример формы регистрации

const schema = {
  type: "object",

  properties: {
    accountType: {
      enum: ["company", "individual"]
    },

    companyName: {
      type: "string"
    },

    passport: {
      type: "string"
    }
  },

  if: {
    properties: {
      accountType: {
        const: "company"
      }
    },

    required: ["accountType"]
  },

  then: {
    required: ["companyName"]
  },

  else: {
    required: ["passport"]
  }
}

Практический пример API

const schema = {
  type: "object",

  properties: {
    method: {
      enum: ["GET", "POST"]
    },

    body: {
      type: "object"
    }
  },

  if: {
    properties: {
      method: {
        const: "POST"
      }
    }
  },

  then: {
    required: ["body"]
  }
}

Типичные ошибки

Отсутствие required в if

Неправильно:

if: {
  properties: {
    type: {
      const: "A"
    }
  }
}

Правильно:

if: {
  required: ["type"],

  properties: {
    type: {
      const: "A"
    }
  }
}

Использование if без then

{
  if: { ... }
}

Условие никак не влияет на результат проверки.


Смешивание логики

Плохо:

then: {
  required: ["a"],
  minimum: 10
}

minimum не применяется к объекту.

Необходимо соблюдать контекст типов.


Рекомендации по проектированию схем

Делать условия узкими

Хорошо:

if: {
  properties: {
    role: {
      const: "admin"
    }
  }
}

Плохо:

if: {
  properties: {
    role: {
      enum: ["admin", "moderator", "editor"]
    }
  }
}

Чем точнее условие, тем проще сопровождение схемы.


Использовать required

Практически любое условие должно включать:

required: [...]

Избегать глубокой вложенности

Слишком большое количество вложенных if-then-else:

  • ухудшает читаемость;
  • усложняет отладку;
  • увеличивает вероятность конфликтов.

Сравнение с oneOf

if-then-else

Подходит для:

  • условных зависимостей;
  • локальных изменений правил;
  • бизнес-логики.

oneOf

Подходит для:

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

Отладка условных схем

Ajv предоставляет массив ошибок:

validate.errors

Для анализа сложных условий удобно:

  • включать allErrors;
  • использовать verbose;
  • выводить промежуточные результаты.

Пример

const ajv = new Ajv({
  allErrors: true,
  verbose: true
})

Использование $ref внутри then

Пример

const schema = {
  definitions: {
    adminSchema: {
      required: ["permissions"]
    }
  },

  if: {
    properties: {
      role: {
        const: "admin"
      }
    }
  },

  then: {
    $ref: "#/definitions/adminSchema"
  }
}

Это позволяет:

  • переиспользовать схемы;
  • уменьшать дублирование;
  • упрощать поддержку больших JSON Schema.