Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ jobs:
python-version: "3.8"
- os: windows-latest
python-version: "3.8"
- os: macos-latest
python-version: "3.9"
- os: windows-latest
python-version: "3.9"
- os: macos-latest
python-version: "3.10"
- os: windows-latest
python-version: "3.10"
- os: macos-latest
python-version: "3.11"
- os: windows-latest
python-version: "3.11"
steps:
- uses: actions/checkout@v3
with:
Expand Down
3 changes: 0 additions & 3 deletions docs/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ Each of the following exceptions has a `token` property, referencing the [`Token
::: jsonpath.JSONPointerError
handler: python

::: jsonpath.JSONPointerEncodeError
handler: python

::: jsonpath.JSONPointerResolutionError
handler: python

Expand Down
35 changes: 31 additions & 4 deletions docs/pointers.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,40 @@

**_New in version 0.8.0_**

JSON Pointer ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) is a string syntax for targeting a single value (JSON object, array or scalar) in a JSON document. Whereas a JSONPath has the potential to yield many values from a JSON document, a JSON Pointer can _resolve_ to at most one value.
JSON Pointer ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) is a string syntax for targeting a single value (JSON object, array, or scalar) within a JSON document. Unlike a JSONPath expression, which can yield multiple values, a JSON Pointer resolves to **at most one value**.

JSON Pointers are a fundamental part of JSON Patch ([RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)). Each patch operation must have at least one pointer, identifying the target value.
JSON Pointers are a fundamental component of JSON Patch ([RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)), where each patch operation must have at least one pointer identifying the target location to modify.

!!! note
??? note "Extensions to RFC 6901"

We have extended RFC 6901 to handle our non-standard JSONPath [keys selector](syntax.md#keys-or) and index/property pointers from [Relative JSON Pointer](#torel).
We have extended RFC 6901 to support:

- Interoperability with the JSONPath [keys selector](syntax.md#keys-or) (`~`)
- A special non-standard syntax for targeting **keys or indices themselves**, used in conjunction with [Relative JSON Pointer](#torel)

**Keys Selector Compatibility**

The JSONPath **keys selector** (`.~` or `[~]`) allows expressions to target the *keys* of an object, rather than their associated values. To maintain compatibility when translating between JSONPath and JSON Pointer, our implementation includes special handling for this selector.

While standard JSON Pointers always refer to values, we ensure that paths derived from expressions like `$.categories.~` can be represented in our pointer system. This is especially important when converting from JSONPath to JSON Pointer or when evaluating expressions that mix value and key access.

**Key/Index Pointers (`#<key or index>`)**

This non-standard pointer form represents **keys or indices themselves**, not the values they map to. Examples:

- `#foo` points to the object key `"foo"` (not the value at `"foo"`)
- `#0` points to the index `0` of an array (not the value at that index)

This syntax is introduced to support the full capabilities of [Relative JSON Pointer](#torel), which allows references to both values and the *keys or indices* that identify them. To ensure that any `RelativeJSONPointer` can be losslessly converted into a `JSONPointer`, we use the `#<key or index>` form to represent these special cases.

#### Example

```python
from jsonpath import RelativeJSONPointer

rjp = RelativeJSONPointer("1#")
print(repr(rjp.to("/items/0/name"))) # JSONPointer('/items/#0')
```

## `resolve(data)`

Expand Down
40 changes: 36 additions & 4 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,22 @@ This page gets you started using JSONPath, JSON Pointer and JSON Patch wih Pytho

## `findall(path, data)`

Find all objects matching a JSONPath with [`jsonpath.findall()`](api.md#jsonpath.JSONPathEnvironment.findall). It takes, as arguments, a JSONPath string and some _data_ object. It always returns a list of objects selected from _data_, never a scalar value.
Find all values matching a JSONPath expression using [`jsonpath.findall()`](api.md#jsonpath.JSONPathEnvironment.findall).

_data_ can be a file-like object or string containing JSON formatted data, or a Python [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) or [`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence), like a dictionary or list. In this example we select user names from a dictionary containing a list of user dictionaries.
This function takes two arguments:

- `path`: a JSONPath expression as a string (e.g., `"$.users[*].name"`)
- `data`: the JSON document to query

It always returns a **list** of matched values, even if the path resolves to a single result or nothing at all.

The `data` argument can be:

- A Python [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) (e.g., `dict`) or [`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence) (e.g., `list`)
- A JSON-formatted string
- A file-like object containing JSON

For example, the following query extracts all user names from a dictionary containing a list of user objects:

```python
import jsonpath
Expand Down Expand Up @@ -173,7 +186,18 @@ if match:

**_New in version 0.8.0_**

Resolve a JSON Pointer ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) against some data. A JSON Pointer references a single object on a specific "path" in a JSON document. Here, _pointer_ can be a string representation of a JSON Pointer or a list of parts that make up a pointer. _data_ can be a file-like object or string containing JSON formatted data, or equivalent Python objects.
Resolves a JSON Pointer ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) against a JSON document, returning the value located at the specified path.

The `pointer` argument can be either:

- A string representation of a JSON Pointer (e.g., `"/foo/bar/0"`)
- A list of unescaped pointer segments (e.g., `["foo", "bar", "0"]`)

The `data` argument can be:

- A Python data structure (`dict`, `list`, etc.)
- A JSON-formatted string
- A file-like object containing JSON

```python
from jsonpath import pointer
Expand Down Expand Up @@ -206,7 +230,15 @@ jane_score = pointer.resolve(["users", 3, "score"], data)
print(jane_score) # 55
```

If the pointer can't be resolved against the target JSON document - due to missing keys/properties or out of range indices - a `JSONPointerIndexError`, `JSONPointerKeyError` or `JSONPointerTypeError` will be raised, each of which inherit from `JSONPointerResolutionError`. A default value can be given, which will be returned in the event of a `JSONPointerResolutionError`.
If the pointer cannot be resolved against the target JSON data — due to a missing key, an out-of-range index, or an unexpected data type — an exception will be raised:

- `JSONPointerKeyError` – when a referenced key is missing from an object
- `JSONPointerIndexError` – when an array index is out of bounds
- `JSONPointerTypeError` – when a path segment expects the wrong type (e.g., indexing into a non-array)

All of these exceptions are subclasses of `JSONPointerResolutionError`.

You can optionally provide a `default` value to `resolve()`, which will be returned instead of raising an error if the pointer cannot be resolved.

```python
from jsonpath import pointer
Expand Down
2 changes: 0 additions & 2 deletions jsonpath/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from .exceptions import JSONPathNameError
from .exceptions import JSONPathSyntaxError
from .exceptions import JSONPathTypeError
from .exceptions import JSONPointerEncodeError
from .exceptions import JSONPointerError
from .exceptions import JSONPointerIndexError
from .exceptions import JSONPointerKeyError
Expand Down Expand Up @@ -52,7 +51,6 @@
"JSONPathSyntaxError",
"JSONPathTypeError",
"JSONPointer",
"JSONPointerEncodeError",
"JSONPointerError",
"JSONPointerIndexError",
"JSONPointerKeyError",
Expand Down
2 changes: 1 addition & 1 deletion jsonpath/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class attributes `root_token`, `self_token` and `filter_context_token`.
intersection_token (str): The pattern used as the intersection operator.
Defaults to `"&"`.
key_token (str): The pattern used to identify the current key or index when
filtering a, mapping or sequence. Defaults to `"#"`.
filtering a mapping or sequence. Defaults to `"#"`.
keys_selector_token (str): The pattern used as the "keys" selector. Defaults to
`"~"`.
lexer_class: The lexer to use when tokenizing path strings.
Expand Down
7 changes: 2 additions & 5 deletions jsonpath/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""JSONPath exceptions."""

from __future__ import annotations

from typing import TYPE_CHECKING
Expand Down Expand Up @@ -80,10 +81,6 @@ class JSONPointerError(Exception):
"""Base class for all JSON Pointer errors."""


class JSONPointerEncodeError(JSONPointerError):
"""An exception raised when a JSONPathMatch can't be encoded to a JSON Pointer."""


class JSONPointerResolutionError(JSONPointerError):
"""Base exception for those that can be raised during pointer resolution."""

Expand Down Expand Up @@ -145,7 +142,7 @@ class JSONPatchTestFailure(JSONPatchError): # noqa: N818
def _truncate_message(value: str, num: int, end: str = "...") -> str:
if len(value) < num:
return value
return f"{value[:num-len(end)]}{end}"
return f"{value[: num - len(end)]}{end}"


def _truncate_words(val: str, num: int, end: str = "...") -> str:
Expand Down
118 changes: 71 additions & 47 deletions jsonpath/pointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

class _Undefined:
def __str__(self) -> str:
return "<jsonpath.pointer.UNDEFINED>"
return "<jsonpath.pointer.UNDEFINED>" # pragma: no cover


UNDEFINED = _Undefined()
Expand Down Expand Up @@ -115,59 +115,83 @@ def _index(self, s: str) -> Union[str, int]:
try:
index = int(s)
if index < self.min_int_index or index > self.max_int_index:
raise JSONPointerIndexError("index out of range")
raise JSONPointerError("index out of range")
return index
except ValueError:
return s

def _getitem(self, obj: Any, key: Any) -> Any: # noqa: PLR0912
def _getitem(self, obj: Any, key: Any) -> Any:
try:
# Handle the most common cases. A mapping with a string key, or a sequence
# with an integer index.
#
# Note that `obj` does not have to be a Mapping or Sequence here. Any object
# implementing `__getitem__` will do.
return getitem(obj, key)
except KeyError as err:
# Try a string repr of the index-like item as a mapping key.
if isinstance(key, int):
try:
return getitem(obj, str(key))
except KeyError:
raise JSONPointerKeyError(key) from err
# Handle non-standard keys/property selector/pointer.
if (
isinstance(key, str)
and isinstance(obj, Mapping)
and key.startswith((self.keys_selector, "#"))
and key[1:] in obj
):
return key[1:]
# Handle non-standard index/property pointer (`#`)
raise JSONPointerKeyError(key) from err
return self._handle_key_error(obj, key, err)
except TypeError as err:
if isinstance(obj, Sequence) and not isinstance(obj, str):
if key == "-":
# "-" is a valid index when appending to a JSON array
# with JSON Patch, but not when resolving a JSON Pointer.
raise JSONPointerIndexError("index out of range") from None
# Handle non-standard index pointer.
if isinstance(key, str) and key.startswith("#"):
_index = int(key[1:])
if _index >= len(obj):
raise JSONPointerIndexError(
f"index out of range: {_index}"
) from err
return _index
# Try int index. Reject non-zero ints that start with a zero.
if isinstance(key, str):
index = self._index(key)
if isinstance(index, int):
try:
return getitem(obj, int(key))
except IndexError as index_err:
raise JSONPointerIndexError(
f"index out of range: {key}"
) from index_err
raise JSONPointerTypeError(f"{key}: {err}") from err
return self._handle_type_error(obj, key, err)
except IndexError as err:
raise JSONPointerIndexError(f"index out of range: {key}") from err

def _handle_key_error(self, obj: Any, key: Any, err: Exception) -> object:
if isinstance(key, int):
# Try a string repr of the index-like item as a mapping key.
return self._getitem(obj, str(key))

# Handle non-standard key/property selector/pointer.
#
# For the benefit of `RelativeJSONPointer.to()` and `JSONPathMatch.pointer()`,
# treat keys starting with a `#` or `~` as a "key pointer". If `key[1:]` is a
# key in `obj`, return the key.
#
# Note that if a key with a leading `#`/`~` exists in `obj`, it will have been
# handled by `_getitem`.
if (
isinstance(key, str)
and isinstance(obj, Mapping)
and key.startswith((self.keys_selector, "#"))
and key[1:] in obj
):
return key[1:]

raise JSONPointerKeyError(key) from err

def _handle_type_error(self, obj: Any, key: Any, err: Exception) -> object:
if (
isinstance(obj, str)
or not isinstance(obj, Sequence)
or not isinstance(key, str)
):
raise JSONPointerTypeError(f"{key}: {err}") from err

# `obj` is array-like
# `key` is a string

if key == "-":
# "-" is a valid index when appending to a JSON array with JSON Patch, but
# not when resolving a JSON Pointer.
raise JSONPointerIndexError("index out of range") from None

# Handle non-standard index pointer.
#
# For the benefit of `RelativeJSONPointer.to()`, treat keys starting with a `#`
# and followed by a valid index as an "index pointer". If `int(key[1:])` is
# less than `len(obj)`, return the index.
if re.match(r"#[1-9]\d*", key):
_index = int(key[1:])
if _index >= len(obj):
raise JSONPointerIndexError(f"index out of range: {_index}") from err
return _index

# Try int index. Reject non-zero ints that start with a zero.
index = self._index(key)
if isinstance(index, int):
return self._getitem(obj, index)

raise JSONPointerTypeError(f"{key}: {err}") from err

def resolve(
self,
data: Union[str, IOBase, Sequence[object], Mapping[str, object]],
Expand Down Expand Up @@ -263,7 +287,7 @@ def from_match(
pointer = cls._encode(match.parts)
else:
# This should not happen, unless the JSONPathMatch has been tampered with.
pointer = ""
pointer = "" # pragma: no cover

return cls(
pointer,
Expand Down Expand Up @@ -328,10 +352,10 @@ def __eq__(self, other: object) -> bool:
return isinstance(other, JSONPointer) and self.parts == other.parts

def __hash__(self) -> int:
return hash(self.parts)
return hash(self.parts) # pragma: no cover

def __repr__(self) -> str:
return f"JSONPointer({self._s!r})"
return f"JSONPointer({self._s!r})" # pragma: no cover

def exists(
self, data: Union[str, IOBase, Sequence[object], Mapping[str, object]]
Expand Down Expand Up @@ -486,7 +510,7 @@ def __eq__(self, __value: object) -> bool:
return isinstance(__value, RelativeJSONPointer) and str(self) == str(__value)

def __hash__(self) -> int:
return hash((self.origin, self.index, self.pointer))
return hash((self.origin, self.index, self.pointer)) # pragma: no cover

def _parse(
self,
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ markdown_extensions:
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
- pymdownx.details

extra_css:
- css/style.css
Expand Down
Loading
Loading