json-schemajsonapivalidation

JSON Schema $ref & Reusable Definitions: A Practical Guide

·9 min read·JSON Schema

DRY schemas with $ref

As JSON Schemas grow, the same shapes appear again and again — an address, a money amount, a user reference. Copy-pasting them is a maintenance trap: change one and you have to find all the others. JSON Schema solves this with `$ref`, a pointer that lets you define a type once and reference it everywhere.

Define once in $defs, reference with $ref

The modern pattern (Draft 2019-09 and 2020-12) puts reusable subschemas under `$defs` and points to them with $ref:

json
{
  "type": "object",
  "properties": {
    "billing":  { "$ref": "#/$defs/address" },
    "shipping": { "$ref": "#/$defs/address" }
  },
  "$defs": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city":   { "type": "string" },
        "country": { "type": "string" }
      },
      "required": ["street", "city", "country"]
    }
  }
}

Both billing and shipping now validate against the same address schema. Fix a typo once and every reference updates. (In older Draft 7, the keyword is definitions and refs look like #/definitions/address.)

How $ref pointers work

A $ref value is a JSON Pointer — a path into the document starting from # (the root):

  • #/$defs/address → the address schema under $defs.
  • #/properties/billing → an existing property's schema.
  • person.json#/$defs/name → a definition in another file.

You can reference definitions in the same document or split shared types into separate files and reference them by URL — handy for a library of common schemas across services.

Reuse across multiple schemas

Put truly shared types (money, timestamps, ids) in one file and reference them everywhere:

json
{
  "properties": {
    "price": { "$ref": "common.json#/$defs/money" }
  }
}

This gives you a single source of truth for cross-cutting types — the schema equivalent of a shared library.

Recursive structures

$ref is the only clean way to model recursion, like a tree or a comment thread. A schema can reference itself:

json
{
  "$defs": {
    "node": {
      "type": "object",
      "properties": {
        "value": { "type": "string" },
        "children": {
          "type": "array",
          "items": { "$ref": "#/$defs/node" }
        }
      }
    }
  },
  "$ref": "#/$defs/node"
}

Each node can contain an array of nodes, to any depth — impossible to express without $ref.

Tips and pitfalls

  • Prefer `$defs` in new schemas; definitions still works for Draft 7 compatibility.
  • Keep refs resolvable. A typo in a pointer (#/$defs/adress) makes validation fail or silently pass depending on the validator.
  • Mind tool support. Most validators (Ajv, Pydantic-via-JSON-Schema) resolve $ref, but external-file refs may need configuration to load.
  • Don't over-abstract. Reuse genuinely shared shapes; inlining a one-off subschema is clearer than a ref used once.

Frequently asked questions

definitions is the Draft 7 keyword; $defs replaces it in Draft 2019-09 and 2020-12. They serve the same purpose — a place to hold reusable subschemas — and both are referenced with $ref.

Yes. Use a URL or relative path before the #, like common.json#/$defs/money. Your validator may need to be configured to load external schema files.

Define the recursive type under $defs and have it reference itself with $ref. That is how you express trees, nested comments, or any arbitrarily deep structure.

Try JSON Schema Generator

Generate a JSON Schema from an example, then add $ref reuse.