При работе с крупными JSON Schema-проектами схема быстро перестаёт помещаться в один файл. Повторяющиеся структуры, переиспользуемые типы, общие описания объектов и независимые модули приводят к необходимости разделения схем на отдельные части.
Ajv поддерживает подключение внешних схем через механизм
$ref, позволяя строить полноценную модульную архитектуру
валидации.
Внешняя схема — это отдельный JSON Schema-документ, подключаемый из
другой схемы через $ref.
Пример:
{
"$ref": "user.schema.json"
}
Ajv воспринимает такую ссылку как указание на другую схему и использует её при валидации.
Основные причины:
Типичная организация схем:
schemas/
├── user.schema.json
├── address.schema.json
├── product.schema.json
├── order.schema.json
└── common/
├── pagination.schema.json
└── error.schema.json
Ajv использует $id для идентификации схемы.
Пример:
{
"$id": "https://example.com/schemas/user.schema.json",
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
$id играет ключевую роль:
$ref;Наиболее распространённый способ подключения внешних схем —
регистрация через addSchema.
{
"$id": "https://example.com/schemas/address.schema.json",
"type": "object",
"properties": {
"city": {
"type": "string"
},
"street": {
"type": "string"
}
},
"required": ["city", "street"]
}
{
"$id": "https://example.com/schemas/user.schema.json",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"address": {
"$ref": "https://example.com/schemas/address.schema.json"
}
},
"required": ["name", "address"]
}
const Ajv = require("ajv")
const ajv = new Ajv()
const addressSchema = require("./schemas/address.schema.json")
const userSchema = require("./schemas/user.schema.json")
ajv.addSchema(addressSchema)
const validate = ajv.compile(userSchema)
const data = {
name: "Alex",
address: {
city: "Berlin",
street: "Main Street"
}
}
const valid = validate(data)
console.log(valid)
Если объект address не соответствует внешней схеме, Ajv
вернёт ошибку.
Ajv поддерживает регистрацию сразу нескольких схем.
ajv.addSchema([
addressSchema,
productSchema,
orderSchema
])
После регистрации каждая схема становится доступной через свой
$id.
Ajv позволяет получать зарегистрированные схемы.
const schema = ajv.getSchema(
"https://example.com/schemas/address.schema.json"
)
Возвращается функция валидатора.
Вместо URL можно использовать локальные идентификаторы.
{
"$id": "address",
"type": "object",
"properties": {
"city": {
"type": "string"
}
}
}
{
"$id": "user",
"properties": {
"address": {
"$ref": "address"
}
}
}
ajv.addSchema(addressSchema)
ajv.addSchema(userSchema)
Ajv поддерживает относительные пути.
{
"$ref": "./address.schema.json"
}
Однако такой подход работает корректно только при правильной настройке загрузки схем.
Во многих проектах предпочтительнее использовать абсолютные
$id.
Наиболее надёжная практика — полноценные URI.
{
"$id": "https://api.example.com/schemas/user"
}
Преимущества:
Ajv различает:
{
"$ref": "#/definitions/address"
}
{
"$ref": "https://example.com/address.schema.json"
}
{
"$id": "common",
"definitions": {
"email": {
"type": "string",
"format": "email"
}
}
}
{
"$id": "user",
"properties": {
"email": {
"$ref": "common#/definitions/email"
}
}
}
Ajv сначала находит внешнюю схему, затем внутренний путь.
Ajv поддерживает автоматическую загрузку отсутствующих схем через
loadSchema.
const Ajv = require("ajv")
const ajv = new Ajv({
loadSchema: async (uri) => {
const response = await fetch(uri)
return response.json()
}
})
const validate = await ajv.compileAsync(userSchema)
compileAsync автоматически загружает отсутствующие
зависимости.
{
"$id": "user",
"properties": {
"address": {
"$ref": "https://example.com/address.schema.json"
}
}
}
const Ajv = require("ajv")
const ajv = new Ajv({
loadSchema: async (uri) => {
const response = await fetch(uri)
return response.json()
}
})
async function run() {
const validate = await ajv.compileAsync(userSchema)
const valid = validate(data)
console.log(valid)
}
run()
Ajv автоматически кэширует загруженные схемы.
Это позволяет:
Удаление схемы:
ajv.removeSchema("user")
Удаление всех схем:
ajv.removeSchema()
Ajv поддерживает рекурсивные структуры.
{
"$id": "tree",
"type": "object",
"properties": {
"value": {
"type": "string"
},
"children": {
"type": "array",
"items": {
"$ref": "tree"
}
}
}
}
Внешние схемы могут ссылаться друг на друга.
{
"$id": "user",
"properties": {
"company": {
"$ref": "company"
}
}
}
{
"$id": "company",
"properties": {
"owner": {
"$ref": "user"
}
}
}
Ajv способен корректно обрабатывать подобные структуры.
При циклических зависимостях рекомендуется заранее регистрировать все схемы.
ajv.addSchema(userSchema)
ajv.addSchema(companySchema)
Одна из самых распространённых ошибок:
can't resolve reference
Основные причины:
$id;$ref;loadSchema.console.log(ajv.schemas)
Полезно при отладке крупных проектов.
В старых версиях JSON Schema использовалось поле id
вместо $id.
Ajv поддерживает настройку:
const ajv = new Ajv({
schemaId: "auto"
})
Важно учитывать версию JSON Schema.
Пример:
{
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
Смешивание разных draft-версий может приводить к ошибкам разрешения ссылок.
Крупные проекты обычно группируют схемы по предметным областям.
schemas/
├── users/
├── billing/
├── products/
├── auth/
└── shared/
Распространённый подход:
const schemas = [
userSchema,
addressSchema,
productSchema,
orderSchema
]
schemas.forEach(schema => {
ajv.addSchema(schema)
})
module.exports = [
require("./user.schema.json"),
require("./address.schema.json"),
require("./product.schema.json")
]
const schemas = require("./schemas")
schemas.forEach(schema => {
ajv.addSchema(schema)
})
Хорошая практика — использовать пространства имён.
{
"$id": "https://example.com/schemas/user/profile"
}
Это помогает:
{
"$id": "https://example.com/schemas/v1/user"
}
ajv.addSchema(userSchemaV1)
ajv.addSchema(userSchemaV2)
Ajv позволяет задавать дополнительное имя схемы.
ajv.addSchema(userSchema, "User")
Использование:
{
"$ref": "User"
}
В production-среде часто заранее компилируют все схемы.
schemas.forEach(schema => {
ajv.compile(schema)
})
Преимущества:
try {
schemas.forEach(schema => {
ajv.compile(schema)
})
console.log("Schemas OK")
} catch (err) {
console.error(err)
}
const fs = require("fs")
const path = require("path")
const dir = path.join(__dirname, "schemas")
fs.readdirSync(dir).forEach(file => {
const schema = require(path.join(dir, file))
ajv.addSchema(schema)
})
В микросервисной архитектуре схемы часто:
Типичная структура:
company-schemas/
├── user/
├── billing/
├── notification/
└── inventory/
OpenAPI активно использует $ref.
Ajv способен валидировать схемы, извлечённые из OpenAPI-спецификаций.
Следует учитывать:
$idПредпочтительно:
{
"$id": "https://example.com/schemas/user"
}
Нежелательно:
{
"$id": "user"
}
Хорошо:
https://example.com/schemas/user
https://example.com/schemas/address
Плохо:
user-schema
AddressSchema
schema/user
Общие типы лучше хранить отдельно:
shared/
common/
definitions/
base/
Хотя Ajv их поддерживает, большое количество циклов усложняет поддержку проекта.
Изменение схемы без версии может сломать старые сервисы.
Особенно важна в production.
schemas/
├── shared/
│ ├── email.schema.json
│ ├── uuid.schema.json
│ └── pagination.schema.json
│
├── users/
│ ├── user.schema.json
│ ├── profile.schema.json
│ └── permissions.schema.json
│
├── orders/
│ ├── order.schema.json
│ └── order-item.schema.json
│
└── products/
├── product.schema.json
└── category.schema.json
{
"$ref": "https://example.com/schemas/shared/email.schema.json"
}
{
"$id": "shared",
"$defs": {
"uuid": {
"type": "string",
"format": "uuid"
}
}
}
{
"$ref": "shared#/$defs/uuid"
}
Старый вариант:
{
"definitions": {}
}
Современный:
{
"$defs": {}
}
На скорость влияют:
$ref;Эффективные практики:
Полезные инструменты:
console.log(validate.errors)
console.log(ajv.schemas)
console.log(schema.$id)
Частая проблема:
{
"$ref": "user"
}
При этом схема зарегистрирована как:
{
"$id": "/user"
}
URI должны совпадать полностью.
if (!ajv.getSchema("user")) {
console.error("Schema not found")
}
Частая практика:
const validators = {
user: ajv.compile(userSchema),
order: ajv.compile(orderSchema)
}
module.exports = {
validateUser,
validateOrder,
validateProduct
}
Это уменьшает число повторных компиляций.
В крупных системах внешние схемы обычно:
@company/schemas
Использование:
const {
userSchema
} = require("@company/schemas")
Внешние схемы часто используются совместно с:
| Метод | Назначение |
|---|---|
addSchema() |
регистрация схем |
getSchema() |
получение валидатора |
removeSchema() |
удаление схем |
compileAsync() |
асинхронная компиляция |
addMetaSchema() |
регистрация meta-schema |
user.schema.json
address.schema.json
$id{
"$id": "https://example.com/schemas/user"
}
ajv.addSchema(schema)
$ref{
"$ref": "https://example.com/schemas/address"
}
const validate = ajv.compile(userSchema)
validate(data)