Библиотека Ajv активно использует механизмы композиции JSON Schema для построения сложных систем валидации. Композиция позволяет собирать большие схемы из независимых модулей, переиспользовать общие части, избегать дублирования и описывать сложные зависимости между структурами данных.
Ключевые инструменты композиции:
allOfanyOfoneOfnot$ref$defsif/then/elseКонструкция allOf требует, чтобы объект одновременно
удовлетворял всем вложенным схемам.
const Ajv = require("ajv")
const ajv = new Ajv()
const schema = {
allOf: [
{
type: "object",
properties: {
id: {
type: "integer"
}
},
required: ["id"]
},
{
type: "object",
properties: {
name: {
type: "string"
}
},
required: ["name"]
}
]
}
const validate = ajv.compile(schema)
console.log(validate({
id: 1,
name: "Alice"
})) // true
Фактически allOf работает как логическое И.
Объект обязан пройти каждую схему.
Один из наиболее распространённых подходов — выделение общей структуры в отдельную схему.
const baseEntity = {
type: "object",
properties: {
id: {
type: "integer"
},
createdAt: {
type: "string",
format: "date-time"
}
},
required: ["id", "createdAt"]
}
const userSchema = {
allOf: [
baseEntity,
{
type: "object",
properties: {
email: {
type: "string",
format: "email"
}
},
required: ["email"]
}
]
}
Преимущества:
Важно понимать, что allOf не объединяет свойства
автоматически на уровне JavaScript-объектов. Каждая схема валидируется
отдельно.
Например:
{
allOf: [
{
additionalProperties: false,
properties: {
a: { type: "string" }
}
},
{
properties: {
b: { type: "string" }
}
}
]
}
Такой код может привести к неожиданным ошибкам, потому что первая схема запрещает свойства, которых она не знает.
Лучше выносить additionalProperties в финальную
объединённую схему.
const schema = {
type: "object",
properties: {
a: { type: "string" },
b: { type: "string" }
},
required: ["a", "b"],
additionalProperties: false
}
anyOf требует соответствия хотя бы одной схеме.
const schema = {
anyOf: [
{
type: "string"
},
{
type: "number"
}
]
}
Допустимые значения:
"hello"
42
Недопустимое значение:
true
Часто API поддерживает несколько вариантов запроса.
const schema = {
anyOf: [
{
type: "object",
properties: {
email: {
type: "string",
format: "email"
}
},
required: ["email"]
},
{
type: "object",
properties: {
phone: {
type: "string"
}
},
required: ["phone"]
}
]
}
Валидны оба варианта:
{ email: "user@mail.com" }
{ phone: "+123456789" }
oneOf требует, чтобы объект соответствовал только одной
схеме.
{
anyOf: [
{ type: "integer" },
{ minimum: 0 }
]
}
Число 10 проходит обе схемы — это допустимо.
{
oneOf: [
{ type: "integer" },
{ minimum: 0 }
]
}
Число 10 невалидно, потому что совпали обе схемы
одновременно.
const schema = {
oneOf: [
{
type: "object",
properties: {
type: { const: "user" },
email: { type: "string" }
},
required: ["type", "email"]
},
{
type: "object",
properties: {
type: { const: "admin" },
permissions: {
type: "array"
}
},
required: ["type", "permissions"]
}
]
}
При большом количестве вариантов oneOf может стать
медленным. Для оптимизации используется discriminator-подход.
const schema = {
oneOf: [
{
properties: {
kind: { const: "circle" },
radius: { type: "number" }
},
required: ["kind", "radius"]
},
{
properties: {
kind: { const: "square" },
size: { type: "number" }
},
required: ["kind", "size"]
}
]
}
Поле kind выступает дискриминатором типа.
not инвертирует результат проверки.
const schema = {
not: {
type: "null"
}
}
Любое значение, кроме null, будет валидным.
const schema = {
type: "object",
properties: {
password: { type: "string" },
token: { type: "string" }
},
not: {
required: ["password", "token"]
}
}
Нельзя одновременно передавать:
{
password: "123",
token: "abc"
}
$defs позволяет хранить локальные переиспользуемые
схемы.
const schema = {
$defs: {
address: {
type: "object",
properties: {
city: { type: "string" },
zip: { type: "string" }
},
required: ["city", "zip"]
}
},
type: "object",
properties: {
home: {
$ref: "#/$defs/address"
},
work: {
$ref: "#/$defs/address"
}
}
}
$ref — фундаментальная часть архитектуры крупных
схем.
{
"$id": "user.schema.json",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
},
"required": ["id", "name"]
}
{
"type": "object",
"properties": {
"title": {
"type": "string"
},
"author": {
"$ref": "user.schema.json"
}
}
}
const Ajv = require("ajv")
const ajv = new Ajv()
ajv.addSchema(userSchema)
const validate = ajv.compile(postSchema)
Ajv поддерживает рекурсивные структуры.
const nodeSchema = {
$id: "node",
type: "object",
properties: {
value: {
type: "string"
},
children: {
type: "array",
items: {
$ref: "node"
}
}
}
}
Условные конструкции позволяют строить динамическую валидацию.
const schema = {
type: "object",
properties: {
role: {
type: "string"
}
},
if: {
properties: {
role: {
const: "admin"
}
}
},
then: {
required: ["permissions"]
},
else: {
not: {
required: ["permissions"]
}
}
}
Часто структура зависит от режима конфигурации.
const schema = {
type: "object",
properties: {
mode: {
enum: ["development", "production"]
}
},
if: {
properties: {
mode: {
const: "production"
}
}
},
then: {
required: ["sslCertificate"]
}
}
Ajv позволяет комбинировать сложные структуры массивов.
const schema = {
type: "array",
prefixItems: [
{ type: "string" },
{ type: "number" }
],
items: false
}
Допустимо:
["age", 25]
Недопустимо:
["age", 25, true]
const schema = {
type: "object",
additionalProperties: {
type: "object",
properties: {
enabled: {
type: "boolean"
}
},
required: ["enabled"]
}
}
Пример данных:
{
featureA: {
enabled: true
},
featureB: {
enabled: false
}
}
dependentSchemas добавляет схему при наличии
определённого поля.
const schema = {
type: "object",
properties: {
creditCard: {
type: "string"
}
},
dependentSchemas: {
creditCard: {
required: ["billingAddress"]
}
}
}
Более лёгкий вариант зависимости.
const schema = {
type: "object",
dependentRequired: {
password: ["confirmPassword"]
}
}
const baseDto = {
type: "object",
properties: {
id: {
type: "integer"
}
},
required: ["id"]
}
const createUserDto = {
allOf: [
baseDto,
{
properties: {
email: {
type: "string"
}
},
required: ["email"]
}
]
}
const updateUserDto = {
allOf: [
baseDto,
{
properties: {
email: {
type: "string"
}
}
}
]
}
Многие плагинообразные системы используют композицию схем.
const pluginSchema = {
type: "object",
properties: {
name: {
type: "string"
},
options: {
type: "object"
}
},
required: ["name"]
}
const loggerPluginSchema = {
allOf: [
pluginSchema,
{
properties: {
name: {
const: "logger"
},
options: {
type: "object",
properties: {
level: {
enum: ["info", "warn", "error"]
}
}
}
}
}
]
}
Слишком глубокая композиция может ухудшать производительность.
allOf;oneOf;$ref;type
kind
category
schemaType
Такие поля резко ускоряют проверку вариантов.
Плохо:
A -> B -> C -> D
Лучше:
A + B + C
Хорошая практика:
$defs:
pagination
user
address
metadata
allOf: [
{
required: ["a"]
},
{
required: ["b"]
}
]
Результат:
required: ["a", "b"]
Это не альтернатива, а накопление требований.
allOf: [
{ type: "string" },
{ type: "number" }
]
Схема никогда не будет валидной.
oneOf: [
{ type: "number" },
{ minimum: 0 }
]
Большинство положительных чисел ломают схему, потому что проходят обе проверки.
Для крупных проектов обычно используется модульная структура.
schemas/
├── common/
│ ├── pagination.json
│ ├── address.json
│ └── error.json
│
├── user/
│ ├── user.json
│ ├── create-user.json
│ └── update-user.json
│
├── post/
│ ├── post.json
│ └── create-post.json
│
└── index.js
Ajv часто используется вместе с TypeScript.
type Shape =
| Circle
| Square
Обычно соответствует:
oneOf: [...]
interface Entity {
id: number
}
Соответствует:
allOf: [...]
Иногда требуется валидировать только часть объекта.
const userSchema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" }
},
required: ["name", "age"]
}
const patchSchema = {
allOf: [
userSchema,
{
required: []
}
]
}
На практике чаще создают отдельную схему без обязательных полей.
const schema = {
allOf: [
{
type: "object",
properties: {
type: {
type: "string"
}
}
},
{
if: {
properties: {
type: {
const: "email"
}
}
},
then: {
required: ["email"]
}
},
{
if: {
properties: {
type: {
const: "sms"
}
}
},
then: {
required: ["phone"]
}
}
]
}
Эффективная композиция обычно строится по следующим принципам:
$ref.oneOf.$defs для локальных компонентов.