Определения через $defs

Ключевое слово $defs используется в спецификации JSON Schema для хранения переиспользуемых схем внутри одного документа. В библиотеке Ajv $defs позволяет:

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

$defs пришёл на смену устаревшему definitions, использовавшемуся в старых версиях JSON Schema.


Базовая структура $defs

Простейшая схема с определением через $defs:

const schema = {
  type: "object",

  properties: {
    user: {
      $ref: "#/$defs/user"
    }
  },

  required: ["user"],

  $defs: {
    user: {
      type: "object",

      properties: {
        name: { type: "string" },
        age: { type: "number" }
      },

      required: ["name", "age"],
      additionalProperties: false
    }
  }
}

Проверка:

import Ajv from "ajv"

const ajv = new Ajv()

const validate = ajv.compile(schema)

console.log(
  validate({
    user: {
      name: "Alex",
      age: 25
    }
  })
)

Как работает $ref

$defs почти всегда используется вместе с $ref.

Пример ссылки:

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

Разбор пути:

Часть Значение
# текущий документ
/$defs переход к разделу $defs
/user схема user

Ajv заменяет $ref содержимым соответствующей схемы.


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

Без $defs:

const schema = {
  type: "object",

  properties: {
    author: {
      type: "object",
      properties: {
        name: { type: "string" },
        email: { type: "string" }
      }
    },

    editor: {
      type: "object",
      properties: {
        name: { type: "string" },
        email: { type: "string" }
      }
    }
  }
}

Проблемы:

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

С $defs:

const schema = {
  type: "object",

  properties: {
    author: {
      $ref: "#/$defs/person"
    },

    editor: {
      $ref: "#/$defs/person"
    }
  },

  $defs: {
    person: {
      type: "object",

      properties: {
        name: { type: "string" },
        email: { type: "string" }
      },

      required: ["name", "email"]
    }
  }
}

Вложенные определения

$defs может содержать множество схем.

const schema = {
  type: "object",

  properties: {
    user: {
      $ref: "#/$defs/user"
    },

    product: {
      $ref: "#/$defs/product"
    }
  },

  $defs: {
    user: {
      type: "object",
      properties: {
        id: { type: "number" },
        name: { type: "string" }
      }
    },

    product: {
      type: "object",
      properties: {
        title: { type: "string" },
        price: { type: "number" }
      }
    }
  }
}

Использование определений внутри других определений

Схемы в $defs могут ссылаться друг на друга.

const schema = {
  $defs: {
    address: {
      type: "object",

      properties: {
        city: { type: "string" },
        street: { type: "string" }
      },

      required: ["city", "street"]
    },

    user: {
      type: "object",

      properties: {
        name: { type: "string" },

        address: {
          $ref: "#/$defs/address"
        }
      },

      required: ["name", "address"]
    }
  },

  type: "object",

  properties: {
    user: {
      $ref: "#/$defs/user"
    }
  }
}

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

const schema = {
  type: "array",

  items: {
    $ref: "#/$defs/product"
  },

  $defs: {
    product: {
      type: "object",

      properties: {
        title: { type: "string" },
        price: { type: "number" }
      },

      required: ["title", "price"]
    }
  }
}

Проверяемые данные:

[
  {
    title: "Phone",
    price: 500
  },
  {
    title: "Laptop",
    price: 1500
  }
]

Глубокая композиция схем

$defs особенно полезен в больших структурах.

const schema = {
  type: "object",

  properties: {
    order: {
      $ref: "#/$defs/order"
    }
  },

  $defs: {
    user: {
      type: "object",

      properties: {
        id: { type: "integer" },
        name: { type: "string" }
      },

      required: ["id", "name"]
    },

    product: {
      type: "object",

      properties: {
        sku: { type: "string" },
        price: { type: "number" }
      },

      required: ["sku", "price"]
    },

    orderItem: {
      type: "object",

      properties: {
        product: {
          $ref: "#/$defs/product"
        },

        quantity: {
          type: "integer",
          minimum: 1
        }
      },

      required: ["product", "quantity"]
    },

    order: {
      type: "object",

      properties: {
        customer: {
          $ref: "#/$defs/user"
        },

        items: {
          type: "array",

          items: {
            $ref: "#/$defs/orderItem"
          }
        }
      },

      required: ["customer", "items"]
    }
  }
}

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

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

Пример дерева категорий:

const schema = {
  $defs: {
    category: {
      type: "object",

      properties: {
        name: {
          type: "string"
        },

        children: {
          type: "array",

          items: {
            $ref: "#/$defs/category"
          }
        }
      },

      required: ["name"]
    }
  },

  $ref: "#/$defs/category"
}

Пример данных:

{
  name: "Electronics",

  children: [
    {
      name: "Phones"
    },
    {
      name: "Laptops",
      children: [
        {
          name: "Gaming"
        }
      ]
    }
  ]
}

Комбинирование с allOf

const schema = {
  $defs: {
    entity: {
      type: "object",

      properties: {
        id: {
          type: "integer"
        }
      },

      required: ["id"]
    },

    userData: {
      type: "object",

      properties: {
        name: {
          type: "string"
        }
      },

      required: ["name"]
    }
  },

  allOf: [
    {
      $ref: "#/$defs/entity"
    },
    {
      $ref: "#/$defs/userData"
    }
  ]
}

Результат:

{
  id: 1,
  name: "Alex"
}

Комбинирование с anyOf

const schema = {
  $defs: {
    emailContact: {
      type: "object",

      properties: {
        email: {
          type: "string",
          format: "email"
        }
      },

      required: ["email"]
    },

    phoneContact: {
      type: "object",

      properties: {
        phone: {
          type: "string"
        }
      },

      required: ["phone"]
    }
  },

  anyOf: [
    {
      $ref: "#/$defs/emailContact"
    },
    {
      $ref: "#/$defs/phoneContact"
    }
  ]
}

Комбинирование с oneOf

const schema = {
  $defs: {
    cardPayment: {
      type: "object",

      properties: {
        cardNumber: {
          type: "string"
        }
      },

      required: ["cardNumber"]
    },

    cashPayment: {
      type: "object",

      properties: {
        cash: {
          const: true
        }
      },

      required: ["cash"]
    }
  },

  oneOf: [
    {
      $ref: "#/$defs/cardPayment"
    },
    {
      $ref: "#/$defs/cashPayment"
    }
  ]
}

Использование с if / then / else

const schema = {
  type: "object",

  properties: {
    type: {
      type: "string"
    }
  },

  if: {
    properties: {
      type: {
        const: "admin"
      }
    }
  },

  then: {
    $ref: "#/$defs/admin"
  },

  else: {
    $ref: "#/$defs/user"
  },

  $defs: {
    admin: {
      properties: {
        accessLevel: {
          type: "number"
        }
      },

      required: ["accessLevel"]
    },

    user: {
      properties: {
        nickname: {
          type: "string"
        }
      },

      required: ["nickname"]
    }
  }
}

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

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

const userSchema = {
  $id: "https://example.com/user.schema.json",

  $defs: {
    profile: {
      type: "object",

      properties: {
        age: {
          type: "number"
        }
      }
    }
  }
}

Регистрация:

ajv.addSchema(userSchema)

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

const schema = {
  $ref: "https://example.com/user.schema.json#/$defs/profile"
}

Отличие $defs от definitions

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

definitions: {
  user: { ... }
}

Современный вариант:

$defs: {
  user: { ... }
}

Причины перехода

  • соответствие новым версиям JSON Schema;
  • единый стиль ключевых слов с $;
  • лучшая совместимость со спецификацией Draft 2019-09 и новее.

Ajv поддерживает оба варианта, но рекомендуется использовать $defs.


Частые ошибки

Ошибка в пути $ref

Неверно:

$ref: "#/defs/user"

Верно:

$ref: "#/$defs/user"

Отсутствие схемы

$ref: "#/$defs/address"

Но:

$defs: {
  user: { ... }
}

Ajv выдаст ошибку компиляции.


Циклические ссылки без рекурсии

Некорректная структура:

a -> b -> a

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


Несовместимость draft-версий

Некоторые проекты используют старые версии JSON Schema:

  • Draft-04
  • Draft-06
  • Draft-07

В них чаще встречается definitions.

Для Draft 2019-09 и Draft 2020-12 рекомендуется $defs.


Оптимизация больших схем

Выделение базовых сущностей

Хорошая практика:

$defs: {
  id: { ... },
  user: { ... },
  product: { ... },
  order: { ... }
}

Минимизация дублирования

Плохо:

properties: {
  user1: { ... },
  user2: { ... },
  user3: { ... }
}

Хорошо:

properties: {
  user1: { $ref: "#/$defs/user" },
  user2: { $ref: "#/$defs/user" },
  user3: { $ref: "#/$defs/user" }
}

Изоляция сложных блоков

Крупные вложенные структуры лучше выносить:

$defs: {
  paymentInfo: { ... }
}

вместо огромных inline-описаний.


Производительность Ajv и $defs

Ajv компилирует схемы в JavaScript-функции.

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

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

Особенно заметна польза в:

  • больших API;
  • микросервисах;
  • OpenAPI-схемах;
  • системах конфигурации;
  • генераторах форм.

Практический пример API-схемы

const schema = {
  type: "object",

  properties: {
    users: {
      type: "array",

      items: {
        $ref: "#/$defs/user"
      }
    }
  },

  $defs: {
    address: {
      type: "object",

      properties: {
        city: {
          type: "string"
        },

        zip: {
          type: "string"
        }
      },

      required: ["city", "zip"]
    },

    user: {
      type: "object",

      properties: {
        id: {
          type: "integer"
        },

        name: {
          type: "string"
        },

        address: {
          $ref: "#/$defs/address"
        }
      },

      required: [
        "id",
        "name",
        "address"
      ]
    }
  }
}

Проверяемые данные:

{
  users: [
    {
      id: 1,
      name: "Alex",

      address: {
        city: "Berlin",
        zip: "10001"
      }
    }
  ]
}

Рекомендации по именованию

Хорошие имена

$defs: {
  user,
  product,
  address,
  paymentMethod
}

Плохие имена

$defs: {
  a1,
  x,
  data2
}

Организация крупных схем

Один из популярных подходов:

$defs: {
  primitives: { ... },
  entities: { ... },
  api: { ... },
  responses: { ... }
}

Либо:

$defs: {
  User,
  Product,
  Order,
  Invoice
}

Главное правило — единообразие структуры.


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

Многие генераторы TypeScript-типов используют $defs.

Например:

$defs: {
  user: {
    type: "object",
    properties: {
      id: { type: "number" }
    }
  }
}

может быть преобразовано в:

type User = {
  id: number
}

Это особенно важно при:

  • генерации SDK;
  • контрактной разработке;
  • OpenAPI;
  • codegen-инструментах.

Связь $defs и OpenAPI

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

Аналог:

components:
  schemas:

По сути выполняет ту же задачу, что и $defs в JSON Schema.

Ajv часто применяется вместе с:

  • Swagger;
  • OpenAPI Generator;
  • Fastify;
  • Express middleware;
  • NestJS validation pipelines.

Когда использовать $defs

$defs особенно полезен, если:

  • схема превышает несколько десятков строк;
  • есть повторяющиеся структуры;
  • используются вложенные объекты;
  • проект содержит множество API-эндпоинтов;
  • необходима масштабируемость схем;
  • схема используется в нескольких местах одновременно.

Для маленьких одноразовых схем применение $defs не всегда оправдано, но в средних и крупных проектах это один из ключевых механизмов организации JSON Schema.