Definitions & References

The JSON Schema specification also allows us to define auxiliary schema in order to be reused and combined later on.

This feature involves two steps: First we need to define the subschemas that shall be used later on, and then we need a standard for calling and reusing these definitions.

Definitions

To establish a difference between the main schema and the auxiliary definitions we adopt the convention that every JSON Schema document consists of two parts. A JSON Schema, and a set of definitions.

For example, suppose we want to define a Schema for documents containing information about names and ages of people. Earlier in this guide we used this schema:

{
    "type": "object",
    "required": ["first_name", "last_name", "age"],
    "properties": {
        "first_name": {"type": "string"},
        "last_name": {"type": "string"},
        "age": {"type": "integer"}
    }
}

It specifies objects that must have a first_name, a last_name and an age. Now we want to create a definition, called person, that represents this schema. We do it as follows:

{
    "definitions": {
        "person": {
            "type": "object",
            "required": ["first_name", "last_name", "age"],
            "properties": {
                "first_name": {"type": "string"},
                "last_name": {"type": "string"},
                "age": {"type": "integer"}
            }
        }
    }
}

We could also have several definitions in a document. Here we add "football_team" to "person":

{
    "definitions": {
        "person": {
            "type": "object",
            "required": ["first_name", "last_name", "age"],
            "properties": {
                "first_name": {"type": "string"},
                "last_name": {"type": "string"},
                "age": {"type": "integer"}
            }
        },
        "football_team": {
            "type": "object",
            "required": ["team", "league"],
            "properties": {
                "team": {"type": "string"},
                "league": {"type": "string"},
                "year_founded": {"type": "integer"}
            }
        }

    }
}

Formal Specification

We can plug in these definitions at the top level of any JSON Schema. The resulting grammar looks as follows, where JSDoc is a JSON Document that contains a Schema, JSch is the primitive used early on in this guide, and JSON can be any JSON document.

JSDoc := { ( defs, )? JSch }
defs := "definitions": { JSON } 

Of course, we will see in a minute that defs will only be usefull if some parts of JSON are actually valid JSON Schemas.

References

Now we get to use the schemas we have defined with definitions. Do we need to specify football players? Easy. Just combine a person with a football team:

{
    "definition": {
        "person": {
            "type": "object",
            "required": ["first_name", "last_name", "age"],
            "properties": {
                "first_name": {"type": "string"},
                "last_name": {"type": "string"},
                "age": {"type": "integer"}
            }
        },
        "football_team": {
            "type": "object",
            "required": ["name", "league"],
            "properties": {
                "name": {"type": "string"},
                "league": {"type": "string"},
                "year_founded": {"type": "integer"}
            }
        }

    }, 
    "allOf": [
        {"$ref": "#/definitions/person"},
        {"$ref": "#/definitions/football_team"} 
    ]
}

Note how this document has two parts. The first part is under the "definitions" keyword, and it simply defines person and football_team. The second part is the actual schema. It specifies documents that satisfy both the schema under person and the schema under football_team. For example, the following object validates against this schema:

{
    "first_name": "Gary",
    "last_name": "Medel",
    "age": 27, 
    "name": "Inter de Milan", 
    "league": "Serie A"
}

Of course, the reuse of "name" here is a bit confusing. We better place the information about the team as a separate object:

{
    "definitions": {
        "person": {
            "type": "object",
            "required": ["first_name", "last_name", "age"],
            "properties": {
                "first_name": {"type": "string"},
                "last_name": {"type": "string"},
                "age": {"type": "integer"}
            }
        },
        "football_team": {
            "type": "object",
            "required": ["name", "league"],
            "properties": {
                "name": {"type": "string"},
                "league": {"type": "string"},
                "year_founded": {"type": "integer"}
            }
        }

    }, 
    "allOf": [
        {"$ref": "#/definitions/person"},
        {
            "type": "object",
            "required": ["current_club"], 
            "properties": {
                "current_club": {"$ref": "#/definitions/football_team"}
            }
        } 
    ]
}

Now this specifies documents such as the following one:

{
    "first_name": "Gary",
    "last_name": "Medel",
    "age": 27, 
    "current_club": {
        "name": "Inter de Milan", 
        "league": "Serie A"
    }
}

Much better!

JSON Pointers

Before we can formally explain how the $ref keyword works we need to talk a bit about JSON pointers. The whole idea of this tool is to use a URI to specify a pointer to a given section of a JSON Document. We just use a fraction of what is defined in the Official Specification.

Formal Specification

For the purprose of JSON pointer, we can think of a URI as a string of the form

uri = ( address )? ( # / JPointer )?

Simply said, address corresponds to any URI that does not use the # symbol, or more precisely to any URI-reference constructed using the following grammar, as defined in the official standard:

address = (scheme : )? hier-part (? query ) 

JSON pointers are specified as follows:

JPointer := ( / path )
path := ( unescaped | escaped )
escaped := ~0 | ~1

Where unescaped can be any character except for / and ~. To represent these characters we use the escaped ~0 for ~and ~1 for /.

For example, /definitions/person is a JSON Pointer, and so is /pointer/0/with/numbers/12 and pointer/with/~1escaped/~0chars. Note how the last is actually a representation of the string pointer/with//escaped/~chars.

If path is a string of the form above, we say that rep(path) is the string resulting of replacing first each character ~1 by / and then each character ~0 by ~.

Json Pointer Evaluation

Let J be a JSON document. JSON Pointers are intended to extract a part of J that is specifically indexed by the pointer. Formally, we define the function EVAL() that takes a JSON Document and a JSON Pointer and delivers a subset of J.

Given a JSON document J that is an object, we use J[k] (for a string k) to represent the value of the key value pair in J whose key is k. Likewise, if J is an array, then J[n] (for a natural number n) corresponds to the n-th element of J. We also say that a keyword k appears in J if J contains a key:value pair of the form k: j', for some document j'.

EVAL(J,JPointer) returns:

Reference Specification

The idea of a reference such as {"$ref": "#/definitions/person"} is to use the schema that is stored under the result of evaluating the pointer /definitions/person under the same document that is defining the JSON Schema.

We could also be looking elsewhere for references. In order to do this we need to add the address part of the URI in the reference keyword.

For example, assume that the URI http://db.ing.puc.cl/exampleschema retrieves the following json document:

{
    "definitions": {
        "person": {
            "type": "object",
            "required": ["first_name", "last_name", "age"],
            "properties": {
                "first_name": {"type": "string"},
                "last_name": {"type": "string"},
                "age": {"type": "integer"}
            }
        },
    },
    "type": "object",
    "required": ["name", "league"],
    "properties": {
        "name": {"type": "string"},
        "league": {"type": "string"},
        "year_founded": {"type": "integer"}
    }
}

We can mix URIs and JSON Pointers: the reference http://db.ing.puc.cl/exampleschema retrieves this entire schema, but the reference http://db.ing.puc.cl/exampleschema#/definitions/person retrieves only

{
    "type": "object",
    "required": ["first_name", "last_name", "age"],
    "properties": {
        "first_name": {"type": "string"},
        "last_name": {"type": "string"},
        "age": {"type": "integer"}
    }
}

Now if we want to specify again the schema for football players, we can do it as follows:

{
    "allOf": [
        {"$ref": "http://db.ing.puc.cl/exampleschema#/definitions/person"},
        {
            "type": "object",
            "required": ["current_club"], 
            "properties": {
            "current_club": {"$ref": "http://db.ing.puc.cl/exampleschema"}
            }
        } 
    ]
}

As mentioned, http://db.ing.puc.cl/exampleschema#/definitions/person retrieves only the schema for persons, while {"$ref": "http://db.ing.puc.cl/exampleschema"} retrieves the entire document (naturally, for validation purproses we are only interested in the schema, not in the definitions portion).

Formal Specification

Note that uriRef below is the same grammar we defined earlier for URIs.

refSch := "$ref": " uriRef " 
uriRef := ( address )? ( # / JPointer )?

Formal Validation

Let R be a JSON Reference of the form "$ref": uriRef. We extend the function EVAL() to work with arbitrary JSON references. We do it as follows.

EVAL(J,R) returns:

Let R be again a JSON Reference and J a JSON document. Assume that the JSON schema document that contains R is S. Then we say that J validates against R under S if EVAL(S,R) returns a schema (not an error) and J validates against EVAL(S,R).

ID

The JSON Schema specification also includes the option of an id keyword. This keywords has many uses, here we only comment on the most simple of all: setting up a unique identifier for the schema.

As an example, look at the use of the id keyword in the following schema:

{
    "id": "http://db.ing.puc.cl/exampleschema", 
    "definitions": {
        "person": {
            "type": "object",
            "required": ["first_name", "last_name", "age"],
            "properties": {
                "first_name": {"type": "string"},
                "last_name": {"type": "string"},
                "age": {"type": "integer"}
            }
        }
    },
    "type": "object",
    "required": ["name", "league"],
    "properties": {
        "name": {"type": "string"},
        "league": {"type": "string"},
        "year_founded": {"type": "integer"}
    }
}

This sets up "http://db.ing.puc.cl/exampleschema" as the identifier for the schema. Some validators may choose to retrieve the above schema instead of looking for the schema that the url http://db.ing.puc.cl/exampleschema is pointing at when faced with the keyword {"$ref": "http://db.ing.puc.cl/exampleschema"}, but this is only optional, and therefore the only safe use of the id keyword is when the id of the schema coincides with the document that the id points at.