JSON Pointer

The JSONPointer class is an implementation of the RFC 6901 JSON Pointer specification. In jschon, JSONPointer is commonly used to represent paths to nodes within JSON and JSONSchema documents, and to extract nodes from those documents. But JSONPointer is designed to work with any JSON-compatible Python object – including, for example, native dict and list objects.

Let’s see how JSONPointer works. We begin with an import:

>>> from jschon import JSON, JSONPointer

Now, consider the following example. This JSON instance describes how characters in JSON object keys must be escaped in order to form reference tokens within an RFC 6901 JSON pointer string:

>>> escape_rule = JSON([
...     {"~": "~0"},
...     {"/": "~1"}
... ])

Nodes within this instance can be referenced in the usual way, as described in the JSON guide:

>>> escape_rule[1]["/"]
JSON('~1')

If we look at the path property of this node, we see that the string representation of the JSON pointer includes the escaped form of the "/" key:

>>> escape_rule[1]["/"].path
JSONPointer('/1/~1')

Now let’s get that path into a variable and inspect it a little more closely:

>>> slash_path = escape_rule[1]["/"].path

A JSONPointer is a Sequence[str], with each item in the sequence being the unescaped object key or array index at the next node down the path to the referenced value:

>>> [key for key in slash_path]
['1', '/']

You can create a list or tuple of keys directly from a JSONPointer instance:

>>> tuple(slash_path)
('1', '/')

Notice that the array index is represented as a string, too. In fact, it matches the key attribute on the corresponding JSON node:

>>> escape_rule[1].key
'1'

To extract the referenced object from a JSON document, we use the evaluate() method:

>>> slash_path.evaluate(escape_rule)
JSON('~1')

So far, we’ve seen how to work with the JSONPointer instance that appears as the path of a JSON node. Now let’s look at how to construct a JSONPointer. Consider the following example document:

>>> doc = {"a": {"b": {"c": {"d": "😎"}}}}

The obvious way to make a JSONPointer that points to the leaf node in this example would be:

>>> JSONPointer('/a/b/c/d')
JSONPointer('/a/b/c/d')

Then, as we’d expect:

>>> JSONPointer('/a/b/c/d').evaluate(doc)
'😎'

But here are a few alternative ways to create the same JSON pointer:

>>> JSONPointer(['a', 'b', 'c', 'd'])
JSONPointer('/a/b/c/d')
>>> JSONPointer(['a'], '/b/c', ['d'])
JSONPointer('/a/b/c/d')
>>> JSONPointer('/a/b', JSONPointer('/c/d'))
JSONPointer('/a/b/c/d')

As you can see, the JSONPointer constructor accepts – and concatenates – any number of arguments. Each argument can be either:

  • an RFC 6901 JSON pointer string (with reserved characters escaped); or

  • an iterable of unescaped keys or array indices – which may itself be a JSONPointer instance.

A special case is the JSONPointer constructed without any args:

>>> JSONPointer()
JSONPointer('')

This represents a reference to an entire document:

>>> JSONPointer().evaluate(doc)
{'a': {'b': {'c': {'d': '😎'}}}}

The / operator provides a convenient way to extend a JSONPointer:

>>> JSONPointer() / 'a' / ('b', 'c', 'd')
JSONPointer('/a/b/c/d')
>>> JSONPointer('/a/b') / JSONPointer('/c/d')
JSONPointer('/a/b/c/d')

It works by copying the left-hand operand (a JSONPointer instance) and appending the right-hand operand (an unescaped key, or an iterable of unescaped keys). Note that JSONPointer is immutable, so each invocation of / produces a new JSONPointer instance.

As a Sequence, JSONPointer supports getting a key by its index:

>>> JSONPointer('/a/b/c/d')[-4]
'a'

And taking a slice into a JSONPointer returns a new JSONPointer instance composed of the specified slice of the original’s keys:

>>> JSONPointer('/a/b/c/d')[1:-1]
JSONPointer('/b/c')