ExJSONPointer (ex_json_pointer v0.3.0)

View Source

An Elixir implementation of RFC 6901 JSON Pointer for locating specific values within JSON documents, and also supports Relative JSON Pointer of the JSON Schema Specification draft-2020-12.

Usage

The JSON pointer string syntax can be represented as a JSON string:

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => "hello"}}}, "/a/b/c")
{:ok, "hello"}

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => "hello"}}}, "/a/b")
{:ok, %{"c" => "hello"}}

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => [1, 2, 3]}}}, "/a/b/c")
{:ok, [1, 2, 3]}

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => [1, 2, 3]}}}, "/a/b/c/2")
{:ok, 3}

iex> ExJSONPointer.resolve(%{"a" => [%{"b" => %{"c" => [1, 2]}}, 2, 3]}, "/a/2")
{:ok, 3}

iex> ExJSONPointer.resolve(%{"a" => [%{"b" => %{"c" => [1, 2]}}, 2, 3]}, "/a/0/b/c/1")
{:ok, 2}

or a URI fragment identifier:

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => "hello"}}}, "#/a/b/c")
{:ok, "hello"}

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => "hello"}}}, "#/a/b")
{:ok, %{"c" => "hello"}}

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => [1, 2, 3]}}}, "#/a/b/c")
{:ok, [1, 2, 3]}

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => [1, 2, 3]}}}, "#/a/b/c/2")
{:ok, 3}

iex> ExJSONPointer.resolve(%{"a" => [%{"b" => %{"c" => [1, 2]}}, 2, 3]}, "#/a/2")
{:ok, 3}

iex> ExJSONPointer.resolve(%{"a" => [%{"b" => %{"c" => [1, 2]}}, 2, 3]}, "#/a/0/b/c/1")
{:ok, 2}

Some cases that a JSON pointer that references a nonexistent value:

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => "hello"}}}, "/a/b/d")
{:error, "not found"}

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => [1, 2, 3]}}}, "/a/b/c/4")
{:error, "not found"}

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => "hello"}}}, "#/a/b/d")
{:error, "not found"}

iex> ExJSONPointer.resolve(%{"a" => %{"b" => %{"c" => [1, 2, 3]}}}, "#/a/b/c/4")
{:error, "not found"}

Some cases that a JSON pointer has some empty reference tokens, and link a $ref test case from JSON Schema Test Suite(draft 2020-12) for reference.

iex> ExJSONPointer.resolve(%{"" => %{"" => 1}}, "/")
{:ok, %{"" => 1}}

iex> ExJSONPointer.resolve(%{"" => %{"" => 1}}, "//")
{:ok, 1}

iex> ExJSONPointer.resolve(%{"" => %{"" => 1, "b" => %{"" => 2}}}, "//b")
{:ok, %{"" => 2}}

iex> ExJSONPointer.resolve(%{"" => %{"" => 1, "b" => %{"" => 2}}}, "//b/")
{:ok, 2}

iex> ExJSONPointer.resolve(%{"" => %{"" => 1, "b" => %{"" => 2}}}, "//b///")
{:error, "not found"}

Invalid JSON pointer syntax:

iex> ExJSONPointer.resolve(%{"a" =>%{"b" => %{"c" => [1, 2, 3]}}}, "a/b")
{:error, "invalid JSON pointer syntax"}

iex> ExJSONPointer.resolve(%{"a" =>%{"b" => %{"c" => [1, 2, 3]}}}, "##/a")
{:error, "invalid JSON pointer syntax"}

Relative JSON Pointer

This library also supports Relative JSON Pointer of the JSON Schema Specification draft-2020-12 which allows you to reference values relative to a specific location within a JSON document.

A relative JSON pointer consists of:

  • A non-negative integer (prefix) that indicates how many levels up to traverse
  • An optional index manipulation (+N or -N) for array elements
  • An optional JSON pointer to navigate from the referenced location
# Sample data
iex> data = %{"foo" => ["bar", "baz"], "highly" => %{"nested" => %{"objects" => true}}}
iex> ExJSONPointer.resolve(data, "/foo/1", "0") # Get the current value (0 levels up)
{:ok, "baz"}
# Get the parent array and access its first element (1 level up, then to index 0)
iex> ExJSONPointer.resolve(data, "/foo/1", "1/0")
{:ok, "bar"}
# Get the previous element in the array (current level, index - 1)
iex> ExJSONPointer.resolve(data, "/foo/1", "0-1")
{:ok, "bar"}
# Go up to the root and access a nested property
iex> ExJSONPointer.resolve(data, "/foo/1", "2/highly/nested/objects")
{:ok, true}
# Get the index of the current element in its array
iex> ExJSONPointer.resolve(data, "/foo/1", "0#")
{:ok, 1}

# Get the key name of a property in an object
iex> data2 = %{"features" => [%{"name" => "environment friendly", "url" => "http://example.com"}]}
iex> ExJSONPointer.resolve(data2, "/features/0/url", "1/name")
{:ok, "environment friendly"}
iex> ExJSONPointer.resolve(data2, "/features/0/url", "2#")
{:ok, "features"}

Please see the test cases for more examples.

Summary

Types

The JSON document to be processed, must be a map.

The JSON Pointer string that follows RFC 6901 specification. Can be either a JSON String Representation (starting with '/') or a URI Fragment Identifier Representation (starting with '#').

The result of resolving a JSON Pointer

Functions

Resolve the JSON document with the given JSON Pointer to find the accompanying value.

Resolve a relative JSON pointer from a starting position within a JSON document.

Resolve a JSON pointer while accumulating state during traversal.

Types

document()

@type document() :: map() | list()

The JSON document to be processed, must be a map.

pointer()

@type pointer() :: String.t()

The JSON Pointer string that follows RFC 6901 specification. Can be either a JSON String Representation (starting with '/') or a URI Fragment Identifier Representation (starting with '#').

result()

@type result() :: {:ok, term()} | {:error, String.t()}

The result of resolving a JSON Pointer:

  • {:ok, term()} - the resolved value on success
  • {:error, String.t()} - when there is an error in pointer syntax or value not found

Functions

resolve(document, pointer)

@spec resolve(document(), pointer()) :: result()

Resolve the JSON document with the given JSON Pointer to find the accompanying value.

The pointer can be either:

  • An empty string ("") or "#" to reference the whole document
  • A JSON String Representation starting with "/"
  • A URI Fragment Identifier Representation starting with "#"

Examples

iex> doc = %{"foo" => %{"bar" => "baz"}}
iex> ExJSONPointer.resolve(doc, "/foo/bar")
{:ok, "baz"}
iex> ExJSONPointer.resolve(doc, "/foo/baz")
{:error, "not found"}
iex> ExJSONPointer.resolve(doc, "##foo")
{:error, "invalid JSON pointer syntax"}

resolve(document, start_json_pointer, relative)

@spec resolve(document(), pointer(), String.t()) :: result()

Resolve a relative JSON pointer from a starting position within a JSON document.

This function implements the Relative JSON Pointer specification as described in draft-bhutton-relative-json-pointer-00.

A relative JSON pointer consists of:

  • A non-negative integer (prefix) that indicates how many levels up to traverse
  • An optional index manipulation (+N or -N) for array elements
  • An optional JSON pointer to navigate from the referenced location

Parameters

  • document: The JSON document to be processed
  • start_json_pointer: A JSON pointer that identifies the starting location within the document
  • relative: The relative JSON pointer to evaluate from the starting location

Examples

iex> data = %{"foo" => ["bar", "baz"], "highly" => %{"nested" => %{"objects" => true}}}
iex> ExJSONPointer.resolve(data, "/foo/1", "0")
{:ok, "baz"}
iex> ExJSONPointer.resolve(data, "/foo/1", "1/0")
{:ok, "bar"}
iex> ExJSONPointer.resolve(data, "/foo/1", "0-1")
{:ok, "bar"}
iex> ExJSONPointer.resolve(data, "/foo/1", "2/highly/nested/objects")
{:ok, true}
iex> ExJSONPointer.resolve(data, "/foo/1", "0#")
{:ok, 1}

resolve_while(document, pointer, acc, resolve_fun)

@spec resolve_while(document(), pointer(), acc, (term(),
                                           String.t(),
                                           {document(), acc} ->
                                             {:cont, {term(), acc}}
                                             | {:halt, term()})) ::
  {term(), acc} | {:error, String.t()}
when acc: term()

Resolve a JSON pointer while accumulating state during traversal.

This function allows you to track the traversal path and accumulate values as the JSON pointer is being resolved. It is designed to be useful for implementing operations that need context about the traversal path, such as relative JSON pointers.

Parameters

  • document: The JSON document to be processed
  • pointer: A JSON pointer that identifies the location within the document
  • acc: An initial accumulator value that will be passed to the resolve function
  • resolve_fun: A function that receives the current value, reference token, and accumulated state and returns either {:cont, {new_value, new_acc}} to continue or {:halt, result} to stop traversal

The resolve_fun receives three arguments:

  • The current value at the reference token
  • The current reference token being processed
  • A tuple containing the processing document and the current accumulator

Examples

iex> data = %{"a" => %{"b" => %{"c" => [10, 20, 30]}}}
iex> init_acc = %{}
iex> fun = fn current, ref_token, {_document, acc} ->
...>   {:cont, {current, Map.put(acc, ref_token, current)}}
...> end
iex> {value, result} = ExJSONPointer.resolve_while(data, "/a/b/c/0", init_acc, fun)
iex> value
10
iex> result["c"]
[10, 20, 30]