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:
{
"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→ theaddressschema 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:
{
"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:
{
"$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;
definitionsstill 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.