Ключевое слово oneOf в JSON Schema используется для
описания взаимоисключающих схем. Значение считается валидным только в
том случае, если оно соответствует ровно одной схеме из списка.
Ajv полностью поддерживает oneOf и активно применяет его
при валидации сложных структур данных: API, DTO, событий, конфигураций,
polymorphic-объектов и discriminated unions.
Базовый синтаксис:
const schema = {
oneOf: [
{ type: "string" },
{ type: "number" }
]
}
В этом примере:
oneOfAjv проверяет все схемы внутри массива oneOf.
Валидация считается успешной только если:
Ошибки возникают в двух случаях:
const Ajv = require("ajv")
const ajv = new Ajv()
const schema = {
oneOf: [
{ type: "string" },
{ type: "integer" }
]
}
const validate = ajv.compile(schema)
console.log(validate("hello")) // true
console.log(validate(10)) // true
console.log(validate(3.14)) // false
Число 3.14 не является integer, поэтому не подходит ни
под одну схему.
Наиболее частый сценарий — разные варианты структуры объекта.
Например, система поддерживает два типа пользователей:
const schema = {
oneOf: [
{
type: "object",
properties: {
type: { const: "person" },
firstName: { type: "string" },
lastName: { type: "string" }
},
required: ["type", "firstName", "lastName"],
additionalProperties: false
},
{
type: "object",
properties: {
type: { const: "company" },
companyName: { type: "string" },
inn: { type: "string" }
},
required: ["type", "companyName", "inn"],
additionalProperties: false
}
]
}
Проверка:
validate({
type: "person",
firstName: "Ivan",
lastName: "Petrov"
})
// true
validate({
type: "company",
companyName: "Tech Corp",
inn: "123456789"
})
// true
const особенно
важенБез уникального идентификатора схемы могут пересекаться.
Проблемный пример:
const schema = {
oneOf: [
{
type: "object",
properties: {
name: { type: "string" }
}
},
{
type: "object",
properties: {
age: { type: "number" }
}
}
]
}
Объект:
{
name: "Alex",
age: 30
}
совпадёт сразу с двумя схемами:
name;age.Результат:
false
Поскольку совпадений два, oneOf считает данные
невалидными.
oneOf от
anyOfanyOfТребует совпадения хотя бы одной схемы.
{
anyOf: [
{ type: "string" },
{ type: "number" }
]
}
Если значение подходит под две схемы одновременно — ошибки нет.
oneOfТребует совпадения ровно одной схемы.
{
oneOf: [
{ type: "integer" },
{ type: "number" }
]
}
Проблема:
10
Результат:
false
const schema = {
oneOf: [
{ type: "number" },
{ minimum: 0 }
]
}
Положительное число:
100
подходит под обе схемы.
Результат:
false
На практике почти всегда добавляется специальное поле-дискриминатор.
Например:
type
kind
role
event
action
Это позволяет сделать схемы полностью взаимоисключающими.
Пример ответа API:
{
"type": "success",
"data": {}
}
или
{
"type": "error",
"message": "Access denied"
}
Схема:
const schema = {
oneOf: [
{
type: "object",
properties: {
type: { const: "success" },
data: { type: "object" }
},
required: ["type", "data"],
additionalProperties: false
},
{
type: "object",
properties: {
type: { const: "error" },
message: { type: "string" }
},
required: ["type", "message"],
additionalProperties: false
}
]
}
oneOf и массивыМожно описывать взаимоисключающие варианты элементов массива.
const schema = {
type: "array",
items: {
oneOf: [
{ type: "string" },
{ type: "number" }
]
}
}
Допустимо:
["a", 1, "b", 2]
Недопустимо:
[true]
const schema = {
oneOf: [
{
type: "object",
properties: {
action: { const: "create" },
payload: { type: "object" }
},
required: ["action", "payload"]
},
{
type: "object",
properties: {
action: { const: "delete" },
id: { type: "number" }
},
required: ["action", "id"]
}
]
}
По умолчанию Ajv возвращает общую ошибку:
console.log(validate.errors)
Пример:
[
{
instancePath: "",
schemaPath: "#/oneOf",
keyword: "oneOf",
params: {
passingSchemas: [0, 1]
},
message: "must match exactly one schema in oneOf"
}
]
const ajv = new Ajv({
allErrors: true
})
Теперь можно увидеть ошибки каждой вложенной схемы.
discriminatorAjv поддерживает discriminator для ускорения выбора схемы.
const ajv = new Ajv({
discriminator: true
})
Пример:
const schema = {
discriminator: {
propertyName: "type"
},
oneOf: [
{
properties: {
type: { const: "cat" },
meow: { type: "boolean" }
},
required: ["type", "meow"]
},
{
properties: {
type: { const: "dog" },
bark: { type: "boolean" }
},
required: ["type", "bark"]
}
]
}
Преимущества:
oneOf внутри свойствconst schema = {
type: "object",
properties: {
value: {
oneOf: [
{ type: "string" },
{ type: "number" }
]
}
}
}
requiredconst schema = {
oneOf: [
{
required: ["email"]
},
{
required: ["phone"]
}
]
}
Проблема:
{
email: "a@test.com",
phone: "123"
}
Объект удовлетворяет обеим схемам.
Результат:
false
notconst schema = {
oneOf: [
{
required: ["email"],
not: {
required: ["phone"]
}
},
{
required: ["phone"],
not: {
required: ["email"]
}
}
]
}
Теперь:
const schema = {
oneOf: [
{
type: "object",
properties: {
event: { const: "login" },
userId: { type: "number" }
},
required: ["event", "userId"]
},
{
type: "object",
properties: {
event: { const: "purchase" },
amount: { type: "number" }
},
required: ["event", "amount"]
}
]
}
if/then/elseИногда oneOf можно заменить условной логикой.
oneOf{
oneOf: [
{
properties: {
type: { const: "a" }
}
},
{
properties: {
type: { const: "b" }
}
}
]
}
if{
if: {
properties: {
type: { const: "a" }
}
},
then: {
required: ["fieldA"]
},
else: {
required: ["fieldB"]
}
}
oneOfoneOf особенно полезен в случаях:
oneOf создаёт
проблемыСамая распространённая ошибка.
oneOf: [
{ type: "number" },
{ minimum: 10 }
]
Число 20 проходит обе схемы.
oneOf: [
{ type: "object" },
{
type: "object",
required: ["id"]
}
]
Любой объект с id совпадёт дважды.
additionalProperties: falseБез ограничения дополнительные поля позволяют схемам пересекаться.
oneOfНаиболее стабильный подход:
const schema = {
oneOf: [
{
type: "object",
properties: {
kind: { const: "A" },
valueA: { type: "string" }
},
required: ["kind", "valueA"],
additionalProperties: false
},
{
type: "object",
properties: {
kind: { const: "B" },
valueB: { type: "number" }
},
required: ["kind", "valueB"],
additionalProperties: false
}
]
}
Ключевые элементы:
const;required;additionalProperties: false.Именно такая комбинация делает схемы действительно взаимоисключающими.
oneOfAjv вынужден проверять все схемы внутри oneOf, чтобы
убедиться:
При большом количестве вариантов это может быть дорого.
Особенно если:
$ref;oneOf;Наиболее эффективный способ.
Чем меньше неоднозначности между схемами, тем быстрее валидация.
Полезны:
const
enum
required
additionalProperties
type
oneOfconst schema = {
oneOf: [
{
properties: {
payload: {
oneOf: [
{ type: "string" },
{ type: "number" }
]
}
}
},
{
properties: {
payload: {
type: "boolean"
}
}
}
]
}
Подобные конструкции быстро усложняют диагностику ошибок.
Полезные настройки Ajv:
const ajv = new Ajv({
allErrors: true,
verbose: true
})
const schema = {
discriminator: {
propertyName: "type"
},
oneOf: [
{
$ref: "#/definitions/Cat"
},
{
$ref: "#/definitions/Dog"
}
]
}
Схемы внутри oneOf должны быть:
Если между схемами возможна неоднозначность, oneOf почти
всегда приведёт к трудноуловимым ошибкам валидации.