Удалённые схемы

Валидация через Ajv опирается на механизм ссылок $ref, позволяющий выносить части JSON Schema в отдельные документы. Эти документы могут находиться как в памяти приложения, так и быть доступны по HTTP(S)-адресам. Поддержка удалённых схем делает возможной модульную архитектуру описаний данных, где каждая схема переиспользуется и хранится независимо.

Разрешение внешних ссылок $ref

При встрече конструкции:

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

Ajv инициирует процесс резолвинга. Поведение зависит от конфигурации экземпляра валидатора:

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

Система разрешения строится вокруг идентификатора схемы $id. Он используется как ключ в реестре:

{
  "$id": "https://example.com/schemas/user.schema.json",
  "type": "object",
  "properties": {
    "id": { "type": "string" }
  }
}

После регистрации такой схемы все $ref на этот $id резолвятся локально без повторных сетевых запросов.


Регистрация удалённых схем

Базовый способ работы с внешними схемами — явная регистрация:

import Ajv from "ajv";

const ajv = new Ajv();

ajv.addSchema({
  $id: "https://example.com/schemas/user.schema.json",
  type: "object",
  properties: {
    id: { type: "string" }
  }
});

После добавления схема становится доступной для всех зависимых определений.

Если $ref указывает на ещё не загруженный ресурс, поведение зависит от режима компиляции.


Асинхронное разрешение схем

Удалённые схемы требуют асинхронной загрузки. Для этого используется compileAsync и функция loadSchema.

const validate = await ajv.compileAsync(schema);

В этом режиме Ajv:

  1. обнаруживает внешний $ref;
  2. вызывает loadSchema(uri);
  3. ожидает результат;
  4. компилирует полученную схему;
  5. кэширует её.

Реализация загрузчика схем

Функция загрузки не встроена в ядро и должна быть предоставлена:

const loadSchema = async (uri) => {
  const response = await fetch(uri);
  if (!response.ok) {
    throw new Error(`Cannot load schema: ${uri}`);
  }
  return await response.json();
};

Передача загрузчика:

const ajv = new Ajv({
  loadSchema
});

В Node.js аналогично используется http, https или fs в зависимости от источника.


Механизм кеширования

Ajv поддерживает внутренний реестр загруженных схем:

  • ключ — $id или URI;
  • значение — скомпилированная функция валидации.

Повторная загрузка одного и того же URI не выполняется. Это критично при глубокой вложенности $ref, где граф зависимостей может быть значительным.

Кеширование снижает:

  • количество сетевых запросов;
  • время компиляции;
  • вероятность циклических зависимостей.

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

Удалённые схемы часто образуют циклы:

// A.schema.json
{ "$ref": "B.schema.json" }

// B.schema.json
{ "$ref": "A.schema.json" }

Ajv обрабатывает такие структуры через отложенную компиляцию. При использовании compileAsync:

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

Без асинхронного режима циклы приводят к ошибке разрешения.


Базовый URI и контекст разрешения

При работе с относительными ссылками учитывается базовый URI:

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

Резолвинг выполняется относительно $id, что формирует итоговый URI:

https://example.com/schemas/user.schema.json

Ошибки в $id приводят к некорректной маршрутизации загрузки и невозможности разрешения зависимостей.


Предзагрузка схем

Для уменьшения количества асинхронных операций схемы могут быть загружены заранее:

await ajv.addSchema(userSchema);
await ajv.addSchema(addressSchema);

или пакетно:

ajv.addSchema([userSchema, addressSchema]);

Такой подход устраняет необходимость обращения к loadSchema во время компиляции.


Производительность при удалённых схемах

Основные факторы, влияющие на производительность:

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

Оптимизация достигается через:

  • локальное хранение критичных схем;
  • агрегацию схем в один пакет;
  • предварительную компиляцию.

Безопасность при загрузке схем

Удалённые схемы создают риск неконтролируемого доступа к ресурсам. Основные угрозы:

  • SSRF через произвольные URL в $ref;
  • подмена схемы внешним сервером;
  • утечка внутренних адресов инфраструктуры.

Ограничение источников загрузки реализуется через фильтрацию:

const loadSchema = async (uri) => {
  if (!uri.startsWith("https://trusted-domain.com/")) {
    throw new Error("Blocked schema source");
  }

  const res = await fetch(uri);
  return res.json();
};

Компиляция и schemaCache

Ajv использует внутренний механизм хранения скомпилированных функций. При повторном использовании схемы происходит:

  • мгновенное получение валидатора;
  • отсутствие повторного анализа JSON Schema;
  • исключение повторного резолвинга $ref.

Это особенно важно при больших графах зависимостей, где число схем может измеряться десятками или сотнями.


Особенности работы с JSON Schema Draft

Удалённые схемы корректно работают в разных версиях спецификации:

  • Draft-07;
  • 2019-09;
  • 2020-12.

Однако поведение $id и $ref может отличаться:

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

Ошибки резолвинга удалённых схем

Типовые проблемы:

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

Пример ошибки:

can't resolve reference https://example.com/schema.json from id #

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


Разделение локальных и удалённых схем

Практика проектирования схем часто комбинирует оба подхода:

  • базовые структуры хранятся локально;
  • доменные модели выносятся в удалённые ресурсы;
  • общие компоненты подключаются через CDN или внутренний registry.

Такое разделение позволяет:

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