hex.pm version

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.