Метасхемы

Метасхема — это схема, описывающая структуру и правила другой JSON Schema. Ajv использует метасхемы для:

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

Фактически метасхема отвечает на вопрос:

«Является ли данная схема корректной схемой?»

Пример обычной схемы:

const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "integer" }
  },
  required: ["name"]
}

Ajv способен проверить не только данные по этой схеме, но и саму схему на соответствие метасхеме JSON Schema Draft.


Как Ajv использует метасхемы

При создании экземпляра Ajv автоматически подключается метасхема определённого стандарта.

const Ajv = require("ajv")

const ajv = new Ajv()

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

const validate = ajv.compile(schema)

Если схема содержит ошибку:

const schema = {
  type: "unknownType"
}

Ajv выбросит исключение:

Error: schema is invalid

Причина — ключ type допускает только значения, определённые метасхемой JSON Schema.


Поле $schema

Ключ $schema определяет, какая метасхема должна использоваться для проверки схемы.

Пример:

const schema = {
  $schema: "https://json-schema.org/draft/2020-12/schema",
  type: "string"
}

Ajv анализирует URI и выбирает соответствующую метасхему.


Поддерживаемые версии JSON Schema

Ajv поддерживает несколько поколений стандарта JSON Schema.

Draft-07

Наиболее распространённая версия.

const Ajv = require("ajv")

const ajv = new Ajv()

Draft-2019-09

Подключается отдельным классом.

const Ajv2019 = require("ajv/dist/2019")

const ajv = new Ajv2019()

Draft-2020-12

Современная версия спецификации.

const Ajv2020 = require("ajv/dist/2020")

const ajv = new Ajv2020()

Отличия метасхем между Draft

Метасхемы разных версий содержат различия в поддерживаемых ключевых словах.

Draft-07

{
  additionalProperties: false
}

Draft-2019-09

Появились:

  • unevaluatedProperties
  • dependentSchemas
  • dependentRequired

Draft-2020-12

Добавлены:

  • prefixItems
  • $dynamicRef
  • $dynamicAnchor

Проверка схемы через validateSchema

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

const valid = ajv.validateSchema(schema)

console.log(valid)

Если схема некорректна:

console.log(ajv.errors)

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

const schema = {
  type: 123
}

Результат:

[
  {
    instancePath: "/type",
    message: "must be equal to one of the allowed values"
  }
]

Отключение проверки схем

Параметр validateSchema позволяет отключить автоматическую проверку.

const ajv = new Ajv({
  validateSchema: false
})

Используется редко, поскольку некорректная схема может привести к неправильной валидации данных.


Добавление собственной метасхемы

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

ajv.addMetaSchema(metaSchema)

Пример:

const metaSchema = {
  $id: "https://example.com/custom-meta",
  type: "object",
  properties: {
    type: {
      enum: ["string", "number"]
    }
  }
}

Подключение:

ajv.addMetaSchema(metaSchema)

После этого схемы могут ссылаться на новую метасхему:

const schema = {
  $schema: "https://example.com/custom-meta",
  type: "string"
}

Отличие addSchema и addMetaSchema

addSchema

Добавляет обычную схему данных.

ajv.addSchema(userSchema, "user")

addMetaSchema

Добавляет схему, описывающую другие схемы.

ajv.addMetaSchema(metaSchema)

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

Каждая метасхема должна иметь уникальный идентификатор.

const metaSchema = {
  $id: "https://example.com/meta",
  type: "object"
}

Ajv использует $id:

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

Ключевое слово $ref в метасхемах

Метасхемы активно используют ссылки.

Пример:

const metaSchema = {
  definitions: {
    stringType: {
      type: "string"
    }
  },

  properties: {
    name: {
      $ref: "#/definitions/stringType"
    }
  }
}

Ajv разрешает ссылки во время компиляции схемы.


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

Метасхемы JSON Schema являются рекурсивными.

Пример:

{
  properties: {
    properties: {
      additionalProperties: {
        $ref: "#"
      }
    }
  }
}

Здесь схема описывает другие схемы того же формата.


Проверка пользовательских ключевых слов

Метасхемы позволяют ограничивать структуру кастомных keyword.

Создание keyword

ajv.addKeyword({
  keyword: "positiveNumber",
  type: "number",
  validate(schema, data) {
    return data > 0
  },
  metaSchema: {
    type: "boolean"
  }
})

Назначение metaSchema в keyword

Поле metaSchema определяет допустимое значение самого keyword.

В примере выше:

metaSchema: {
  type: "boolean"
}

означает:

{
  positiveNumber: true
}

разрешено, а:

{
  positiveNumber: 123
}

вызовет ошибку схемы.


Метасхемы и пользовательские форматы

Форматы могут участвовать в проверке схем.

ajv.addFormat("only-a", {
  type: "string",
  validate: value => /^a+$/.test(value)
})

Метасхема JSON Schema определяет, что format должен быть строкой.

Поэтому:

{
  format: 123
}

является некорректной схемой.


Режим strict

Strict Mode усиливает проверку схем метасхемой.

const ajv = new Ajv({
  strict: true
})

Ajv начинает:

  • запрещать неизвестные keyword;
  • обнаруживать неоднозначные конструкции;
  • предупреждать о потенциальных ошибках.

Неизвестные ключевые слова

Пример:

const schema = {
  type: "string",
  unknowKeyword: true
}

В strict mode:

Error: unknown keyword

Отключение strict mode

const ajv = new Ajv({
  strict: false
})

Метасхемы и OpenAPI

Ajv способен работать со схемами OpenAPI.

OpenAPI использует модифицированный вариант JSON Schema.

Для совместимости применяется опция:

const ajv = new Ajv({
  discriminator: true
})

Некоторые инструменты добавляют собственные метасхемы OpenAPI.


Асинхронные схемы и метасхемы

Метасхема может разрешать использование асинхронных keyword.

ajv.addKeyword({
  keyword: "idExists",
  async: true,
  type: "number",
  validate: async (schema, data) => {
    return true
  }
})

При этом схема должна содержать:

{
  $async: true
}

Корректность $async также проверяется метасхемой.


Использование нескольких метасхем

Ajv способен одновременно хранить несколько метасхем.

ajv.addMetaSchema(meta1)
ajv.addMetaSchema(meta2)

Выбор происходит через $schema.

{
  $schema: "https://example.com/meta1"
}

Получение метасхемы

Метасхему можно получить через getSchema.

const meta = ajv.getSchema(
  "https://json-schema.org/draft/2020-12/schema"
)

Удаление схем и метасхем

ajv.removeSchema("https://example.com/meta")

Ajv удаляет схему из внутреннего реестра.


Внутреннее устройство метасхем JSON Schema

Метасхема JSON Schema сама является JSON Schema.

Фрагмент официальной метасхемы:

{
  type: ["object", "boolean"],

  properties: {
    type: {
      anyOf: [
        { $ref: "#/$defs/simpleTypes" },
        {
          type: "array",
          items: {
            $ref: "#/$defs/simpleTypes"
          }
        }
      ]
    }
  }
}

Здесь:

  • схема описывает допустимое значение type;
  • допускается строка;
  • допускается массив строк.

Булевы схемы и метасхемы

JSON Schema допускает использование boolean вместо объекта.

true

означает:

{}

false

означает:

{
  not: {}
}

Метасхема обязана учитывать этот синтаксис.


Диалекты JSON Schema

Современные версии JSON Schema используют понятие dialect.

Диалект определяет:

  • набор keyword;
  • семантику валидации;
  • правила обработки ссылок;
  • поведение аннотаций.

Ajv ориентируется на URI метасхемы.


Метасхемы и $defs

В новых версиях JSON Schema используется $defs.

{
  $defs: {
    positiveInt: {
      type: "integer",
      minimum: 1
    }
  }
}

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

{
  $ref: "#/$defs/positiveInt"
}

Метасхема определяет корректность структуры $defs.


Ограничение структуры schema object

Метасхемы позволяют ограничивать допустимые свойства схем.

Пример:

const metaSchema = {
  type: "object",
  properties: {
    type: {
      type: "string"
    }
  },
  additionalProperties: false
}

Теперь схема:

{
  type: "string",
  unknown: true
}

станет некорректной.


Компиляция метасхем

Ajv компилирует метасхемы так же, как обычные схемы.

Это означает:

  • генерацию JavaScript-кода;
  • оптимизацию проверок;
  • кэширование;
  • повторное использование валидаторов.

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

Проверка схемы метасхемой происходит:

  • при compile;
  • при addSchema;
  • при addMetaSchema.

Для больших проектов это может влиять на скорость запуска приложения.

Иногда применяют:

validateSchema: false

но только при полном контроле над схемами.


Типичные ошибки при работе с метасхемами

Несовместимый Draft

{
  $schema: "http://json-schema.org/draft-04/schema#"
}

Ajv последних версий не поддерживает Draft-04 без дополнительных пакетов.


Отсутствие $id

const metaSchema = {
  type: "object"
}

Без $id могут возникнуть проблемы со ссылками.


Неверный тип keyword

{
  minimum: "10"
}

Метасхема требует число.


Неизвестный keyword

{
  myKeyword: true
}

В strict mode схема станет невалидной.


Практический пример пользовательской метасхемы

const Ajv = require("ajv")

const ajv = new Ajv()

const metaSchema = {
  $id: "https://example.com/app-meta",

  type: "object",

  properties: {
    type: {
      enum: ["string", "number", "boolean"]
    },

    description: {
      type: "string"
    }
  },

  required: ["type"],

  additionalProperties: false
}

ajv.addMetaSchema(metaSchema)

const schema = {
  $schema: "https://example.com/app-meta",
  type: "string",
  description: "User name"
}

console.log(
  ajv.validateSchema(schema)
)

Практический пример ошибки метасхемы

const invalidSchema = {
  $schema: "https://example.com/app-meta",
  type: "array"
}

Результат:

false

Ошибки:

console.log(ajv.errors)
[
  {
    instancePath: "/type",
    message: "must be equal to one of the allowed values"
  }
]

Архитектурная роль метасхем

Метасхемы являются фундаментом экосистемы JSON Schema и Ajv.

Они обеспечивают:

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