Ключевое слово $ref используется для повторного
применения схем JSON Schema без дублирования структуры. Вместо
копирования одинаковых описаний объектов схема может ссылаться на другую
схему или её часть.
Ajv полностью поддерживает механизм ссылок JSON Schema и активно использует его для:
Базовый пример:
const Ajv = require("ajv")
const ajv = new Ajv()
const schema = {
definitions: {
user: {
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string" }
},
required: ["id", "name"]
}
},
type: "object",
properties: {
author: {
$ref: "#/definitions/user"
}
}
}
const validate = ajv.compile(schema)
console.log(validate({
author: {
id: 1,
name: "Alex"
}
}))
Результат:
true
Наиболее распространённый вариант — ссылка на внутренний раздел схемы через JSON Pointer.
{
$ref: "#/definitions/user"
}
Символ # означает текущий документ.
Путь:
#/definitions/user
указывает на:
definitions: {
user: { ... }
}
definitionsВ старых версиях JSON Schema обычно использовалось поле
definitions.
const schema = {
definitions: {
address: {
type: "object",
properties: {
city: { type: "string" },
zip: { type: "string" }
},
required: ["city", "zip"]
}
},
type: "object",
properties: {
shippingAddress: {
$ref: "#/definitions/address"
},
billingAddress: {
$ref: "#/definitions/address"
}
}
}
Обе структуры используют одну и ту же схему.
Это уменьшает:
$defs вместо
definitionsВ новых версиях JSON Schema (2019-09,
2020-12) вместо definitions используется
$defs.
const schema = {
$defs: {
product: {
type: "object",
properties: {
id: { type: "integer" },
title: { type: "string" }
},
required: ["id", "title"]
}
},
type: "array",
items: {
$ref: "#/$defs/product"
}
}
Ajv поддерживает оба варианта.
Крупные проекты обычно разделяют схемы на отдельные модули.
{
"$id": "https://example.com/schemas/user.json",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
},
"required": ["id", "name"]
}
{
"type": "object",
"properties": {
"title": {
"type": "string"
},
"author": {
"$ref": "https://example.com/schemas/user.json"
}
}
}
$id в AjvAjv использует $id как уникальный адрес схемы.
const userSchema = {
$id: "https://example.com/schemas/user.json",
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string" }
}
}
После регистрации схемы:
ajv.addSchema(userSchema)
она становится доступной через $ref.
addSchemaAjv должен знать все схемы, на которые существуют ссылки.
const Ajv = require("ajv")
const ajv = new Ajv()
ajv.addSchema(userSchema)
const validate = ajv.compile(postSchema)
Если ссылка указывает на неизвестную схему, Ajv выбросит ошибку.
ajv.addSchema([
userSchema,
addressSchema,
productSchema
])
Схему можно зарегистрировать под собственным именем.
ajv.addSchema(userSchema, "user")
Использование:
{
$ref: "user"
}
$ref{
$ref: "https://example.com/schemas/user.json"
}
$ref{
$ref: "user.json"
}
Относительные ссылки вычисляются относительно $id.
const schema = {
$id: "https://example.com/schemas/post.json",
properties: {
author: {
$ref: "user.json"
}
}
}
Ajv преобразует ссылку в:
https://example.com/schemas/user.json
{
$ref: "user.json#/properties/address"
}
Сначала загружается схема user.json, затем выбирается
раздел:
properties.address
$ref позволяет описывать рекурсивные модели.
Пример дерева категорий:
const categorySchema = {
$id: "category",
type: "object",
properties: {
name: {
type: "string"
},
children: {
type: "array",
items: {
$ref: "category"
}
}
}
}
Проверка:
const validate = ajv.compile(categorySchema)
validate({
name: "Root",
children: [
{
name: "Child"
}
]
})
const schema = {
$id: "node",
type: "object",
properties: {
value: {
type: "string"
},
next: {
$ref: "node"
}
}
}
Ajv умеет корректно обрабатывать циклические $ref.
Пример:
A -> B
B -> A
Однако подобные схемы могут усложнять:
Типичная ошибка:
MissingRefError: can't resolve reference user.json
Причины:
$id;getSchemaconst schema = ajv.getSchema("user")
или:
const schema = ajv.getSchema(
"https://example.com/schemas/user.json"
)
$refВо время compile() Ajv:
Поэтому производительность Ajv остаётся высокой даже при большом
количестве $ref.
Ajv может:
Поведение зависит от:
inlineRefsconst ajv = new Ajv({
inlineRefs: true
})
Варианты:
inlineRefs: true
inlineRefs: false
inlineRefs: 10
trueМелкие схемы встраиваются в код.
falseКаждая ссылка становится отдельной функцией.
Лимит размера схемы для инлайнинга.
loadSchemaAjv умеет автоматически загружать внешние схемы.
const ajv = new Ajv({
loadSchema: async (uri) => {
const response = await fetch(uri)
return response.json()
}
})
При использовании loadSchema применяется:
await ajv.compileAsync(schema)
const Ajv = require("ajv")
const ajv = new Ajv({
loadSchema: async (uri) => {
const response = await fetch(uri)
return response.json()
}
})
const schema = {
$ref: "https://example.com/user.json"
}
async function run() {
const validate = await ajv.compileAsync(schema)
console.log(validate({
id: 1,
name: "John"
}))
}
run()
$anchorСовременный JSON Schema поддерживает $anchor.
{
$defs: {
user: {
$anchor: "user",
type: "object",
properties: {
name: {
type: "string"
}
}
}
}
}
Ссылка:
{
$ref: "#user"
}
$dynamicRef и
$dynamicAnchorНовые версии JSON Schema поддерживают динамические ссылки.
{
$dynamicAnchor: "node"
}
{
$dynamicRef: "#node"
}
Эти механизмы используются для сложной рекурсии и расширяемых схем.
Ajv поддерживает их в современных draft-версиях.
$ref и
allOf$refПолностью заменяет текущую схему.
{
$ref: "#/$defs/user"
}
allOfКомбинирует схемы.
{
allOf: [
{ $ref: "#/$defs/user" },
{
properties: {
active: {
type: "boolean"
}
}
}
]
}
$refВ draft-07 и старых версиях JSON Schema:
{
$ref: "#/$defs/user",
type: "object"
}
поле type будет проигнорировано.
Работает только $ref.
Это одна из наиболее распространённых ошибок.
allOfПравильный вариант:
{
allOf: [
{
$ref: "#/$defs/user"
},
{
type: "object",
properties: {
active: {
type: "boolean"
}
}
}
]
}
Для крупных проектов обычно создаются:
schemas/
├── user.json
├── product.json
├── order.json
├── address.json
└── payment.json
Каждая схема:
$id;$ref.// user.schema.json
{
"$id": "user",
"$defs": {
"id": {
"type": "integer"
}
},
"type": "object",
"properties": {
"id": {
"$ref": "#/$defs/id"
}
}
}
const schema = {
$defs: {
email: {
type: "string",
format: "email"
}
},
type: "object",
properties: {
contactEmail: {
$ref: "#/$defs/email"
}
}
}
const schema = {
$defs: {
tags: {
type: "array",
items: {
type: "string"
}
}
},
properties: {
labels: {
$ref: "#/$defs/tags"
}
}
}
Ошибка:
{
$ref: "#/defs/user"
}
если раздел называется $defs.
Правильно:
{
$ref: "#/$defs/user"
}
JSON Pointer использует специальные правила:
| Символ | Замена |
|---|---|
~ |
~0 |
/ |
~1 |
Пример:
{
$ref: "#/$defs/a~1b"
}
соответствует ключу:
"a/b"
module.exports = {
$id: "user",
type: "object",
properties: {
id: { type: "integer" }
}
}
const Ajv = require("ajv")
const userSchema = require("./userSchema")
const ajv = new Ajv()
ajv.addSchema(userSchema)
Плохо:
$defs -> common -> entities -> user
Лучше:
$defs -> user
Слишком глубокая структура усложняет:
$ref.$idНельзя:
{
$id: "user"
}
в нескольких схемах одновременно.
Ajv использует $id как уникальный идентификатор.
Ошибка:
$id: "user schema"
Желательно использовать корректные URI:
$id: "https://example.com/schemas/user.json"
или:
$id: "user"
Некоторые возможности $ref зависят от версии JSON
Schema.
Например:
definitions — старый стиль;$defs — новый стиль;$dynamicRef доступен только в новых draft.Ajv поддерживает:
Версия влияет на:
$ref;validateSchemaconst valid = ajv.validateSchema(schema)
console.log(valid)
Ajv способен проверить корректность самих $ref.
console.log(validate.errors)
Результат:
[
{
instancePath: "/author",
schemaPath: "#/properties/author/type",
keyword: "type",
message: "must be object"
}
]
Даже если ошибка возникла внутри $ref, Ajv показывает
полный путь.
$refСсылки превращают JSON Schema в систему взаимосвязанных модулей.
Преимущества:
В больших приложениях $ref становится фундаментом всей
структуры валидации данных.