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
...> )
3Use 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
A reducer callback used by batch_resolve_reduce/4.
It receives:
- The original JSON pointer string.
- The result produced for that pointer.
- The current accumulator.
It must return the updated accumulator.
The JSON document to be processed, must be a map.
@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 '#').
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
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"}
}
@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:
- The original pointer string.
- The result for that pointer.
- 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"}}
]
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"}
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 (
+Nor-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}
@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:
- The current value found at the reference token.
- The current reference token (key or index) being processed.
- 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 withnew_valueas the context for the next token andnew_accas the updated accumulator.{:halt, result}: Stops traversal immediately and returnsresult.
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]
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
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)
- A hash character
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