ExJSONPointer (ex_json_pointer v0.6.1)

Copy Markdown 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}

If the existing JSON pointer points to a nil value, then it will return {:ok, nil} in this case.

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

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"}

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

Batch Resolve

When you need to resolve multiple JSON pointers against the same document, you can use batch_resolve/2.

This is especially useful when several pointers share common prefixes, because the implementation can reuse part of the traversal work internally. For sparse pointer sets with little shared structure, it will fall back to resolving pointers individually.

iex> doc = %{
...>   "users" => %{
...>     "1" => %{
...>       "profile" => %{"name" => "alice", "email" => "alice@example.com"},
...>       "settings" => %{"theme" => "dark"}
...>     }
...>   }
...> }
iex> ExJSONPointer.batch_resolve(doc, ["/users/1/profile/name", "/users/1/profile/email", "/users/1/settings/theme"])
%{
  "/users/1/profile/name" => {:ok, "alice"},
  "/users/1/profile/email" => {:ok, "alice@example.com"},
  "/users/1/settings/theme" => {:ok, "dark"}
}

It also reports errors per pointer in the returned map:

iex> ExJSONPointer.batch_resolve(%{"foo" => "bar"}, ["", "#", "foo", "/missing"])
%{
  "" => {:ok, %{"foo" => "bar"}},
  "#" => {:ok, %{"foo" => "bar"}},
  "foo" => {:error, "invalid JSON pointer syntax"},
  "/missing" => {:error, "not found"}
}

If you do not need the full %{pointer => result} map, you can use batch_resolve_reduce/4 to reduce results directly into your own accumulator. This can be a better fit when you want to count matches, collect only successful values, or stream results into a custom structure while avoiding an additional result map allocation.

iex> doc = %{
...>   "users" => %{
...>     "1" => %{
...>       "profile" => %{"name" => "alice", "email" => "alice@example.com"}
...>     }
...>   }
...> }
iex> ExJSONPointer.batch_resolve_reduce(
...>   doc,
...>   ["/users/1/profile/name", "/users/1/profile/email", "/users/2/profile/name"],
...>   %{},
...>   fn pointer, result, acc ->
...>     case result do
...>       {:ok, value} -> Map.put(acc, pointer, value)
...>       {:error, _reason} -> acc
...>     end
...>   end
...> )
%{
  "/users/1/profile/name" => "alice",
  "/users/1/profile/email" => "alice@example.com"
}

You can also use it for lightweight aggregations when you only care about derived information:

iex> doc = %{
...>   "items" => [
...>     %{"name" => "first"},
...>     %{"name" => "second"}
...>   ]
...> }
iex> ExJSONPointer.batch_resolve_reduce(
...>   doc,
...>   ["/items/0/name", "/items/1/name", "/items/2/name", "/items/1#"],
...>   0,
...>   fn _pointer, result, acc ->
...>     case result do
...>       {:ok, _value} -> acc + 1
...>       {:error, _reason} -> acc
...>     end
...>   end
...> )
3

Use batch_resolve/2 when you want the complete result map. Use batch_resolve_reduce/4 when you want to process each result immediately into a custom accumulator.

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

A reducer callback used by batch_resolve_reduce/4.

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

Resolves multiple JSON Pointers against a JSON document in a batch.

Resolves multiple JSON Pointers against a JSON document and reduces the results with a callback.

Resolves a value from a JSON document using a JSON Pointer.

Resolves a relative JSON pointer starting from a specific location within a JSON document.

Traverses a JSON document using a JSON pointer, maintaining an accumulator.

Validates if the given string follows the JSON Pointer format (RFC 6901, section 5).

Validates if the given string follows the Relative JSON Pointer format.

Types

batch_reduce_fun(acc)

@type batch_reduce_fun(acc) :: (pointer(), result(), acc -> acc)

A reducer callback used by batch_resolve_reduce/4.

It receives:

  1. The original JSON pointer string.
  2. The result produced for that pointer.
  3. The current accumulator.

It must return the updated accumulator.

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

batch_resolve(document, pointers)

@spec batch_resolve(document(), [pointer()]) :: %{required(pointer()) => result()}

Resolves multiple JSON Pointers against a JSON document in a batch.

This function is useful when you need to resolve a set of pointers against the same document and some of those pointers share common prefixes. In those cases, the implementation may reuse part of the traversal work instead of resolving every pointer fully and independently.

The function uses an adaptive strategy internally:

  • for smaller batches, or for batches with enough shared leading tokens, it uses a grouped traversal strategy;
  • for sparse batches with little shared prefix overlap, it falls back to resolving each pointer individually.

The returned value is always a map keyed by the original pointer strings, where each value is the same kind of result returned by resolve/2.

Parameters

  • document: The JSON document to be processed.
  • pointers: A list of JSON pointer strings.

Notes

  • An empty string "" or "#" resolves to the whole document.
  • Invalid pointer syntax is reported per pointer as {:error, "invalid JSON pointer syntax"}.
  • Missing values are reported per pointer as {:error, "not found"}.
  • When duplicate pointers are given, the returned map contains a single entry for that pointer key, following normal map semantics.

Examples

iex> doc = %{"foo" => %{"bar" => "baz", "qux" => "corge"}}
iex> ExJSONPointer.batch_resolve(doc, ["/foo/bar", "/foo/qux", "/foo/unknown"])
%{
  "/foo/bar" => {:ok, "baz"},
  "/foo/qux" => {:ok, "corge"},
  "/foo/unknown" => {:error, "not found"}
}

iex> doc = %{"users" => %{"1" => %{"profile" => %{"name" => "alice", "email" => "alice@example.com"}}}}
iex> ExJSONPointer.batch_resolve(doc, ["/users/1/profile/name", "/users/1/profile/email"])
%{
  "/users/1/profile/name" => {:ok, "alice"},
  "/users/1/profile/email" => {:ok, "alice@example.com"}
}

iex> doc = %{"foo" => "bar"}
iex> ExJSONPointer.batch_resolve(doc, ["", "#", "foo"])
%{
  "" => {:ok, %{"foo" => "bar"}},
  "#" => {:ok, %{"foo" => "bar"}},
  "foo" => {:error, "invalid JSON pointer syntax"}
}

batch_resolve_reduce(document, pointers, acc, reduce_fun)

@spec batch_resolve_reduce(document(), [pointer()], acc, batch_reduce_fun(acc)) :: acc
when acc: term()

Resolves multiple JSON Pointers against a JSON document and reduces the results with a callback.

This function is useful when you want to process each batch result directly into a custom accumulator instead of always materializing the full %{pointer => result} map returned by batch_resolve/2.

The reducer callback receives:

  1. The original pointer string.
  2. The result for that pointer.
  3. The current accumulator.

It must return the updated accumulator.

Parameters

  • document: The JSON document to be processed.
  • pointers: A list of JSON pointer strings.
  • acc: The initial accumulator.
  • reduce_fun: A reducer callback invoked for each pointer result.

Examples

iex> doc = %{"users" => %{"1" => %{"profile" => %{"name" => "alice", "email" => "alice@example.com"}}}}
iex> ExJSONPointer.batch_resolve_reduce(
...>   doc,
...>   ["/users/1/profile/name", "/users/1/profile/email", "/users/2/profile/name"],
...>   %{},
...>   fn pointer, result, acc ->
...>     case result do
...>       {:ok, value} -> Map.put(acc, pointer, value)
...>       {:error, _reason} -> acc
...>     end
...>   end
...> )
%{
  "/users/1/profile/name" => "alice",
  "/users/1/profile/email" => "alice@example.com"
}

iex> doc = %{"foo" => "bar"}
iex> ExJSONPointer.batch_resolve_reduce(
...>   doc,
...>   ["", "#", "foo"],
...>   [],
...>   fn pointer, result, acc -> [{pointer, result} | acc] end
...> )
...> |> Enum.reverse()
[
  {"", {:ok, %{"foo" => "bar"}}},
  {"#", {:ok, %{"foo" => "bar"}}},
  {"foo", {:error, "invalid JSON pointer syntax"}}
]

resolve(document, pointer)

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

Resolves a value from a JSON document using a JSON Pointer.

Implements RFC 6901.

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()

Resolves a relative JSON pointer starting from a specific location within a JSON document.

Implements the Relative JSON Pointer specification (e.g. draft-handrews-relative-json-pointer-01).

A relative JSON pointer consists of:

  • A non-negative integer (prefix) indicating how many levels up to traverse.
  • An optional index manipulation (+N or -N) for array elements.
  • An optional JSON pointer to navigate downwards from the referenced location.
  • Or a # to retrieve the key/index of the current value.

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 string to evaluate.

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()

Traverses a JSON document using a JSON pointer, maintaining an accumulator.

This function is similar to Enum.reduce_while/3 but follows the path of a JSON Pointer. It allows tracking the traversal path and accumulating values as the pointer is resolved. This is useful for implementing operations that require context about the traversal path, such as Relative JSON Pointers.

Parameters

  • document: The JSON document to be processed.
  • pointer: A JSON pointer string.
  • acc: An initial accumulator value.
  • resolve_fun: A callback function invoked for each segment of the pointer.

The Callback Function

The resolve_fun receives three arguments:

  1. The current value found at the reference token.
  2. The current reference token (key or index) being processed.
  3. A tuple {current_document, accumulator} containing the document context at the current level and the accumulator.

It must return one of:

  • {:cont, {new_value, new_acc}}: Continues traversal with new_value as the context for the next token and new_acc as the updated accumulator.
  • {:halt, result}: Stops traversal immediately and returns result.

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, acc} = ExJSONPointer.resolve_while(data, "/a/b/c/0", init_acc, fun)
iex> value
10
iex> acc["c"]
[10, 20, 30]

valid_json_pointer?(pointer)

@spec valid_json_pointer?(pointer()) :: boolean()

Validates if the given string follows the JSON Pointer format (RFC 6901, section 5).

According to JSON Schema specification (draft 2020-12), the json-pointer format expects the string to be a valid JSON String Representation of a JSON Pointer. This means it must either be an empty string or start with a slash /. URI Fragment Identifier Representations (starting with #) are not considered valid for this format check.

It also validates that tilde ~ characters are properly escaped as ~0 (for ~) or ~1 (for /).

Examples

iex> ExJSONPointer.valid_json_pointer?("/foo/bar")
true

iex> ExJSONPointer.valid_json_pointer?("/foo/bar~0/baz~1/%a")
true

iex> ExJSONPointer.valid_json_pointer?("")
true

iex> ExJSONPointer.valid_json_pointer?("/")
true

iex> ExJSONPointer.valid_json_pointer?("/foo//bar")
true

iex> ExJSONPointer.valid_json_pointer?("/~1.1")
true

iex> ExJSONPointer.valid_json_pointer?("/foo/bar~")
false

iex> ExJSONPointer.valid_json_pointer?("/~2")
false

iex> ExJSONPointer.valid_json_pointer?("#")
false

iex> ExJSONPointer.valid_json_pointer?("some/path")
false

valid_relative_json_pointer?(pointer)

@spec valid_relative_json_pointer?(String.t()) :: boolean()

Validates if the given string follows the Relative JSON Pointer format.

This implements the validation for the relative-json-pointer format from JSON Schema. A relative JSON pointer consists of:

  • A non-negative integer prefix (0, 1, 2...)
  • An optional index manipulation (+/- and integer) (e.g., +1, -1)
  • Followed by either:
    • A hash character #
    • A JSON Pointer (starting with / or empty)

Examples

iex> ExJSONPointer.valid_relative_json_pointer?("1")
true

iex> ExJSONPointer.valid_relative_json_pointer?("0/foo/bar")
true

iex> ExJSONPointer.valid_relative_json_pointer?("0#")
true

iex> ExJSONPointer.valid_relative_json_pointer?("/foo/bar")
false