Внешние схемы

При работе с крупными JSON Schema-проектами схема быстро перестаёт помещаться в один файл. Повторяющиеся структуры, переиспользуемые типы, общие описания объектов и независимые модули приводят к необходимости разделения схем на отдельные части.

Ajv поддерживает подключение внешних схем через механизм $ref, позволяя строить полноценную модульную архитектуру валидации.


Что такое внешняя схема

Внешняя схема — это отдельный JSON Schema-документ, подключаемый из другой схемы через $ref.

Пример:

{
  "$ref": "user.schema.json"
}

Ajv воспринимает такую ссылку как указание на другую схему и использует её при валидации.


Зачем разделять схемы

Основные причины:

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

Структура проекта

Типичная организация схем:

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;
  • помогает Ajv находить зависимости;
  • предотвращает конфликты имён.

Подключение схем через addSchema

Наиболее распространённый способ подключения внешних схем — регистрация через addSchema.

address.schema.json

{
  "$id": "https://example.com/schemas/address.schema.json",
  "type": "object",
  "properties": {
    "city": {
      "type": "string"
    },
    "street": {
      "type": "string"
    }
  },
  "required": ["city", "street"]
}

user.schema.json

{
  "$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"]
}

Javascript

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 можно использовать локальные идентификаторы.

address.schema.json

{
  "$id": "address",
  "type": "object",
  "properties": {
    "city": {
      "type": "string"
    }
  }
}

user.schema.json

{
  "$id": "user",
  "properties": {
    "address": {
      "$ref": "address"
    }
  }
}

Подключение

ajv.addSchema(addressSchema)
ajv.addSchema(userSchema)

Относительные ссылки

Ajv поддерживает относительные пути.

{
  "$ref": "./address.schema.json"
}

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

Во многих проектах предпочтительнее использовать абсолютные $id.


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

Наиболее надёжная практика — полноценные URI.

{
  "$id": "https://api.example.com/schemas/user"
}

Преимущества:

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

Внутренние и внешние ссылки

Ajv различает:

Внутренние ссылки

{
  "$ref": "#/definitions/address"
}

Внешние ссылки

{
  "$ref": "https://example.com/address.schema.json"
}

Комбинация внутренних и внешних ссылок

common.schema.json

{
  "$id": "common",
  "definitions": {
    "email": {
      "type": "string",
      "format": "email"
    }
  }
}

user.schema.json

{
  "$id": "user",
  "properties": {
    "email": {
      "$ref": "common#/definitions/email"
    }
  }
}

Ajv сначала находит внешнюю схему, затем внутренний путь.


Асинхронная загрузка схем

Ajv поддерживает автоматическую загрузку отсутствующих схем через loadSchema.


Настройка 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 автоматически загружает отсутствующие зависимости.


Пример полной асинхронной загрузки

user.schema.json

{
  "$id": "user",
  "properties": {
    "address": {
      "$ref": "https://example.com/address.schema.json"
    }
  }
}

Javascript

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 автоматически кэширует загруженные схемы.

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

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

Ручное управление кэшем

Удаление схемы:

ajv.removeSchema("user")

Удаление всех схем:

ajv.removeSchema()

Рекурсивные внешние схемы

Ajv поддерживает рекурсивные структуры.

tree.schema.json

{
  "$id": "tree",
  "type": "object",
  "properties": {
    "value": {
      "type": "string"
    },
    "children": {
      "type": "array",
      "items": {
        "$ref": "tree"
      }
    }
  }
}

Циклические зависимости

Внешние схемы могут ссылаться друг на друга.

user.schema.json

{
  "$id": "user",
  "properties": {
    "company": {
      "$ref": "company"
    }
  }
}

company.schema.json

{
  "$id": "company",
  "properties": {
    "owner": {
      "$ref": "user"
    }
  }
}

Ajv способен корректно обрабатывать подобные структуры.


Предварительная регистрация зависимостей

При циклических зависимостях рекомендуется заранее регистрировать все схемы.

ajv.addSchema(userSchema)
ajv.addSchema(companySchema)

Ошибка unresolved reference

Одна из самых распространённых ошибок:

can't resolve reference

Основные причины:

  • схема не зарегистрирована;
  • неверный $id;
  • ошибка в $ref;
  • несовпадение URI;
  • неправильный путь;
  • отсутствие loadSchema.

Проверка зарегистрированных схем

console.log(ajv.schemas)

Полезно при отладке крупных проектов.


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

В старых версиях JSON Schema использовалось поле id вместо $id.

Ajv поддерживает настройку:

const ajv = new Ajv({
  schemaId: "auto"
})

Внешние схемы и draft-версии

Важно учитывать версию 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)
})

Индексный файл схем

schemas/index.js

module.exports = [
  require("./user.schema.json"),
  require("./address.schema.json"),
  require("./product.schema.json")
]

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

const schemas = require("./schemas")

schemas.forEach(schema => {
  ajv.addSchema(schema)
})

namespaced URI

Хорошая практика — использовать пространства имён.

{
  "$id": "https://example.com/schemas/user/profile"
}

Это помогает:

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

Версионирование схем

Пример URI с версией

{
  "$id": "https://example.com/schemas/v1/user"
}

Поддержка нескольких версий

ajv.addSchema(userSchemaV1)
ajv.addSchema(userSchemaV2)

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

Ajv позволяет задавать дополнительное имя схемы.

ajv.addSchema(userSchema, "User")

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

{
  "$ref": "User"
}

Компиляция заранее

В production-среде часто заранее компилируют все схемы.

schemas.forEach(schema => {
  ajv.compile(schema)
})

Преимущества:

  • раннее обнаружение ошибок;
  • ускорение runtime;
  • проверка ссылочной целостности.

Проверка ссылок при старте приложения

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)
})

Внешние схемы в микросервисах

В микросервисной архитектуре схемы часто:

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

Общий репозиторий схем

Типичная структура:

company-schemas/
├── user/
├── billing/
├── notification/
└── inventory/

Совместимость с OpenAPI

OpenAPI активно использует $ref.

Ajv способен валидировать схемы, извлечённые из OpenAPI-спецификаций.


Ограничения внешних схем

Следует учитывать:

  • сложность разрешения зависимостей;
  • проблемы циклических ссылок;
  • различия draft-версий;
  • увеличение времени компиляции;
  • необходимость синхронизации URI.

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

Использование абсолютных $id

Предпочтительно:

{
  "$id": "https://example.com/schemas/user"
}

Нежелательно:

{
  "$id": "user"
}

Единый стиль URI

Хорошо:

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"
}

Использование $defs во внешних схемах

shared.schema.json

{
  "$id": "shared",
  "$defs": {
    "uuid": {
      "type": "string",
      "format": "uuid"
    }
  }
}

user.schema.json

{
  "$ref": "shared#/$defs/uuid"
}

Современный подход вместо definitions

Старый вариант:

{
  "definitions": {}
}

Современный:

{
  "$defs": {}
}

Производительность внешних схем

На скорость влияют:

  • число $ref;
  • глубина вложенности;
  • количество схем;
  • размер графа зависимостей;
  • асинхронная загрузка.

Оптимизация

Эффективные практики:

  • предварительная регистрация;
  • кэширование;
  • сокращение глубины ссылок;
  • отказ от избыточной рекурсии;
  • централизованная компиляция.

Отладка внешних схем

Полезные инструменты:

console.log(validate.errors)
console.log(ajv.schemas)
console.log(schema.$id)

Диагностика URI

Частая проблема:

{
  "$ref": "user"
}

При этом схема зарегистрирована как:

{
  "$id": "/user"
}

URI должны совпадать полностью.


Проверка существования схемы

if (!ajv.getSchema("user")) {
  console.error("Schema not found")
}

Разделение compile и validate

Частая практика:

const validators = {
  user: ajv.compile(userSchema),
  order: ajv.compile(orderSchema)
}

Хранение валидаторов

module.exports = {
  validateUser,
  validateOrder,
  validateProduct
}

Это уменьшает число повторных компиляций.


Архитектура enterprise-проектов

В крупных системах внешние схемы обычно:

  • публикуются как npm-пакеты;
  • версионируются semver;
  • тестируются отдельно;
  • используются несколькими командами;
  • интегрируются в CI/CD.

Пример npm-пакета схем

@company/schemas

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

const {
  userSchema
} = require("@company/schemas")

Интеграция с TypeScript

Внешние схемы часто используются совместно с:

  • TypeBox;
  • json-schema-to-ts;
  • OpenAPI Generator;
  • TypeScript DTO;
  • runtime validation.

Основные методы Ajv для работы с внешними схемами

Метод Назначение
addSchema() регистрация схем
getSchema() получение валидатора
removeSchema() удаление схем
compileAsync() асинхронная компиляция
addMetaSchema() регистрация meta-schema

Типичный workflow

1. Создание схем

user.schema.json
address.schema.json

2. Назначение $id

{
  "$id": "https://example.com/schemas/user"
}

3. Регистрация

ajv.addSchema(schema)

4. Использование $ref

{
  "$ref": "https://example.com/schemas/address"
}

5. Компиляция

const validate = ajv.compile(userSchema)

6. Валидация

validate(data)