JSON Schema $ref and $defs
$ref is JSON Schema's mechanism for reuse: instead of repeating a sub-schema in multiple places, you define it once in $defs and reference it with $ref. This keeps schemas DRY, enables recursive structures like trees and linked lists, and allows splitting large schemas across multiple files. This guide covers $defs, $ref syntax, recursive schemas, anchors, and multi-file references.
Want to validate a schema that uses $ref? Jsonic's JSON Schema Validator resolves local $defs references automatically and shows the exact error path.
What is $defs and why use it
$defs is the "definitions section" at the top of a JSON Schema document. It holds named sub-schemas that can be referenced anywhere in the same document with $ref. The schemas inside $defs are not applied automatically — they only take effect when something references them.
The primary benefit is eliminating repetition. In the example below, both billingAddress and shippingAddress reuse the same Address schema defined once in $defs:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"zip": { "type": "string", "pattern": "^\\d{5}$" }
},
"required": ["street", "city", "zip"]
}
},
"type": "object",
"properties": {
"name": { "type": "string" },
"billingAddress": { "$ref": "#/$defs/Address" },
"shippingAddress": { "$ref": "#/$defs/Address" }
},
"required": ["name", "billingAddress"]
}Both billingAddress and shippingAddress are validated against the same Address schema. If the address rules change, you update one definition and both properties instantly pick up the change.
$ref syntax — local and external
A $ref value is a URI or JSON Pointer string. The most common forms are:
// Local reference to $defs (most common)
{ "$ref": "#/$defs/MyType" }
// Reference by $anchor
{ "$ref": "#MyAnchorName" }
// Reference to the root schema
{ "$ref": "#" }
// External file (relative URL)
{ "$ref": "./address.schema.json" }
// External URL
{ "$ref": "https://example.com/schemas/v1/address.json" }The #/$defs/MyType form is a JSON Pointer starting with # (the document root), followed by the path /$defs/MyType. The #MyAnchorName form uses an $anchor identifier instead of a path, making references resilient to structural changes. The bare # points back to the root schema, which is the basis for recursive schemas. External references require the validator to support URI resolution and to have access to the referenced document at validation time.
Multiple $ref in a schema
A single schema can define several types in $defs and reference them throughout the properties. This is the standard pattern for domain objects with shared primitive types like identifiers, timestamps, and constrained strings:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Timestamp": {
"type": "string",
"format": "date-time"
},
"UserId": {
"type": "integer",
"minimum": 1
},
"Tag": {
"type": "string",
"minLength": 1,
"maxLength": 50
}
},
"type": "object",
"properties": {
"id": { "$ref": "#/$defs/UserId" },
"createdAt": { "$ref": "#/$defs/Timestamp" },
"updatedAt": { "$ref": "#/$defs/Timestamp" },
"tags": {
"type": "array",
"items": { "$ref": "#/$defs/Tag" }
}
}
}Notice that createdAt and updatedAt both reference Timestamp. You can also use $ref inside array items, additionalProperties, allOf branches, or anywhere else a schema is expected.
Recursive schemas with $ref
Because $ref is resolved lazily at validation time, a schema can reference itself to describe recursive data structures like trees, nested menus, or linked lists. Define the recursive type in $defs and have it reference itself:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"TreeNode": {
"type": "object",
"properties": {
"value": { "type": "integer" },
"children": {
"type": "array",
"items": { "$ref": "#/$defs/TreeNode" }
}
},
"required": ["value"]
}
},
"$ref": "#/$defs/TreeNode"
}The following data is valid against this schema:
{
"value": 1,
"children": [
{ "value": 2, "children": [] },
{
"value": 3,
"children": [
{ "value": 4 }
]
}
]
}Each node must have a value integer. The children array is optional, and each child is itself a TreeNode. Validators follow the reference lazily for each level of nesting, so there is no schema-level recursion limit — the data just needs to be finite.
Combining $ref with other keywords (Draft 2019-09+)
In Draft 07 and earlier, $ref was exclusive: all sibling keywords in the same schema object were silently ignored. Starting with Draft 2019-09, $ref behaves like any other keyword — siblings are applied alongside it.
// Draft 07: $ref ignores siblings — DON'T do this
{
"$ref": "#/$defs/Address",
"description": "ignored in draft-07"
}
// Draft 2019-09+: $ref can have siblings
{
"$ref": "#/$defs/Address",
"description": "Billing address for the order",
"examples": [{ "street": "123 Main St", "city": "Boston", "zip": "02101" }]
}
// Use allOf for constraints + $ref in any draft
{
"allOf": [
{ "$ref": "#/$defs/Address" },
{ "properties": { "country": { "type": "string" } } }
]
}The allOf pattern is the safest approach when you need to extend a referenced schema with extra constraints, because it works in every draft version. In Draft 2019-09+ you can also add description, title, and examples as direct siblings of $ref without wrapping them in allOf.
$anchor for stable references
A JSON Pointer reference like #/$defs/Address breaks if you rename the key or move the definition to a different location in the document. $anchor (introduced in Draft 2019-09) gives a definition a stable name that is independent of its position:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"address": {
"$anchor": "Address",
"type": "object",
"properties": {
"city": { "type": "string" }
}
}
},
"properties": {
"home": { "$ref": "#Address" },
"work": { "$ref": "#Address" }
}
}The reference "$ref": "#Address" resolves to the schema that has "$anchor": "Address", regardless of where it lives in the document. If you later reorganize your $defs or move the definition to a nested location, all anchor-based references continue to work without modification.
$defs vs definitions — draft compatibility
The table below summarizes how the definitions keyword evolved across JSON Schema drafts. AJV supports both $defs and definitions; use $defs for all new schemas.
| Keyword | Spec status | Draft support |
|---|---|---|
$defs | Official (Draft 2019-09+) | Draft 2019-09, Draft 2020-12 |
definitions | Convention (never official) | Draft 04, 06, 07 (widely supported) |
$anchor | Official (Draft 2019-09+) | Draft 2019-09, Draft 2020-12 |
$id | Official | All drafts |
If you are targeting validators that only support Draft 07 (such as older versions of AJV or some online validators), use definitions instead of $defs. The syntax of $ref pointers changes accordingly: #/definitions/Address instead of #/$defs/Address.
Validator support — AJV example
AJV is the most widely used JSON Schema validator for JavaScript and Node.js. It supports $defs and $ref out of the box for Draft 2020-12:
import Ajv from 'ajv';
const ajv = new Ajv();
const schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$defs: {
Name: { type: 'string', minLength: 1, maxLength: 100 },
},
type: 'object',
properties: {
firstName: { $ref: '#/$defs/Name' },
lastName: { $ref: '#/$defs/Name' },
},
required: ['firstName', 'lastName'],
};
const validate = ajv.compile(schema);
console.log(validate({ firstName: 'Alice', lastName: 'Smith' })); // true
console.log(validate({ firstName: '', lastName: 'Smith' })); // false (minLength)
console.log(validate.errors);AJV resolves the $ref at compile time, so there is no runtime resolution overhead per validation call. For external file references, use ajv.addSchema(externalSchema) before compiling the root schema.
Frequently asked questions
What is $ref in JSON Schema?
$ref is a keyword that references another schema definition by URI or JSON Pointer. When a validator encounters $ref, it resolves the reference and applies the referenced schema to the current instance. $ref can point to a local definition in $defs, a different location in the same document using a JSON Pointer (e.g., #/$defs/Address), or an external file or URL.
What is the difference between $defs and definitions in JSON Schema?
$defs is the canonical keyword in JSON Schema Draft 2019-09 and later (including Draft 2020-12). definitions was the conventional keyword in Draft 07 and earlier — it was never officially part of the spec but was widely supported. Both work the same way: they hold named sub-schemas that $ref can point to. For new schemas, use $defs. For compatibility with older validators, definitions still works.
Can $ref be used alongside other keywords?
In Draft 07 and earlier, $ref was exclusive — all sibling keywords were ignored. In Draft 2019-09 and later, $ref can be combined with other keywords in the same schema object. This allows patterns like adding a description or title alongside a $ref, or using $ref inside an allOf alongside additional constraints.
How do I create a recursive schema with $ref?
Point $ref back to the parent schema or a named $defs entry. For example, a tree node schema can reference itself via { "$ref": "#/$defs/TreeNode" }. Define the recursive type in $defs and reference it from the root or from the items keyword of an array property. The validator follows references lazily, so recursive schemas are valid as long as the data is finite.
How do I reference a schema in a different file?
Use a relative URI in $ref: { "$ref": "./address.schema.json" } or an absolute URL: { "$ref": "https://example.com/schemas/v1/address.json" }. The validator must support reference resolution, and you need to pass the referenced schema to the validator at runtime. AJV supports this via addSchema(); Zod and TypeScript-first tools handle it differently.
How do I avoid breaking $ref when I restructure my schema?
Use $anchor to give a definition a stable name independent of its location: { "$anchor": "Address", ... }. Reference it with { "$ref": "#Address" }. If you restructure the schema and move the definition to a different $defs path, the anchor reference still works without updating all callers. Anchors require Draft 2019-09 or later.
Validate JSON Schema with $ref in Jsonic
Paste any schema that uses $ref and $defs into Jsonic's JSON Schema Validator to check that all references resolve correctly and that your data passes validation.