Skip to content

Commit c07dbd1

Browse files
committed
fix: handling of top-level string only JSON documents
1 parent eacfe54 commit c07dbd1

File tree

6 files changed

+52
-38
lines changed

6 files changed

+52
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
**Fixes**
1010

1111
- Fixed a bug with the parsing of JSON Pointers. When given an arbitrary string without slashes, `JSONPointer` would resolve to the document root. The empty string is the only valid pointer that should resolve to the document root. We now raise a `JSONPointerError` in such cases. See [#27](https://github.com/jg-rp/python-jsonpath/issues/27).
12+
- Fixed handling of JSON documents containing only a top-level string.
1213

1314
**Features**
1415

jsonpath/patch.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
from abc import ABC
77
from abc import abstractmethod
88
from io import IOBase
9+
from typing import Any
910
from typing import Dict
1011
from typing import Iterable
1112
from typing import List
1213
from typing import Mapping
1314
from typing import MutableMapping
1415
from typing import MutableSequence
16+
from typing import Sequence
1517
from typing import TypeVar
1618
from typing import Union
1719

@@ -513,7 +515,7 @@ def test(self: Self, path: Union[str, JSONPointer], value: object) -> Self:
513515
def apply(
514516
self,
515517
data: Union[str, IOBase, MutableSequence[object], MutableMapping[str, object]],
516-
) -> Union[MutableSequence[object], MutableMapping[str, object]]:
518+
) -> object:
517519
"""Apply all operations from this patch to _data_.
518520
519521
If _data_ is a string or file-like object, it will be loaded with
@@ -535,14 +537,7 @@ def apply(
535537
JSONPatchTestFailure: When a _test_ operation does not pass.
536538
`JSONPatchTestFailure` is a subclass of `JSONPatchError`.
537539
"""
538-
if isinstance(data, str):
539-
_data: Union[
540-
MutableSequence[object], MutableMapping[str, object]
541-
] = json.loads(data)
542-
elif isinstance(data, IOBase):
543-
_data = json.loads(data.read())
544-
else:
545-
_data = data
540+
_data = _load_data(data)
546541

547542
for i, op in enumerate(self.ops):
548543
try:
@@ -603,3 +598,18 @@ def apply(
603598
unicode_escape=unicode_escape,
604599
uri_decode=uri_decode,
605600
).apply(data)
601+
602+
603+
def _load_data(
604+
data: Union[int, str, IOBase, Sequence[Any], MutableMapping[str, Any]]
605+
) -> Any:
606+
if isinstance(data, str):
607+
try:
608+
return json.loads(data)
609+
except json.JSONDecodeError:
610+
data = data.strip()
611+
if data.startswith('"') and data.endswith('"'):
612+
return data
613+
if isinstance(data, IOBase):
614+
return json.loads(data.read())
615+
return data

jsonpath/path.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,7 @@ def finditer(
114114
JSONPathTypeError: If a filter expression attempts to use types in
115115
an incompatible way.
116116
"""
117-
# TODO: _load_data()
118-
# - possibly a scalar value
119-
if isinstance(data, str):
120-
_data = json.loads(data)
121-
elif isinstance(data, IOBase):
122-
_data = json.loads(data.read())
123-
else:
124-
_data = data
125-
117+
_data = _load_data(data)
126118
matches: Iterable[JSONPathMatch] = [
127119
JSONPathMatch(
128120
filter_context=filter_context or {},
@@ -160,12 +152,7 @@ async def finditer_async(
160152
filter_context: Optional[FilterContextVars] = None,
161153
) -> AsyncIterable[JSONPathMatch]:
162154
"""An async version of `finditer()`."""
163-
if isinstance(data, str):
164-
_data = json.loads(data)
165-
elif isinstance(data, IOBase):
166-
_data = json.loads(data.read())
167-
else:
168-
_data = data
155+
_data = _load_data(data)
169156

170157
async def root_iter() -> AsyncIterable[JSONPathMatch]:
171158
yield self.env.match_class(
@@ -421,3 +408,16 @@ async def _achain(*iterables: AsyncIterable[T]) -> AsyncIterable[T]:
421408
for it in iterables:
422409
async for element in it:
423410
yield element
411+
412+
413+
def _load_data(data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]]) -> Any:
414+
if isinstance(data, str):
415+
try:
416+
return json.loads(data)
417+
except json.JSONDecodeError:
418+
data = data.strip()
419+
if data.startswith('"') and data.endswith('"'):
420+
return data
421+
if isinstance(data, IOBase):
422+
return json.loads(data.read())
423+
return data

jsonpath/pointer.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,7 @@ def resolve(
190190
JSONPointerTypeError: When attempting to resolve a non-index string
191191
path part against a sequence, unless a default is given.
192192
"""
193-
if isinstance(data, str):
194-
data = json.loads(data)
195-
elif isinstance(data, IOBase):
196-
data = json.loads(data.read())
193+
data = _load_data(data)
197194
try:
198195
return reduce(self._getitem, self.parts, data)
199196
except JSONPointerResolutionError:
@@ -227,13 +224,7 @@ def resolve_parent(
227224
if not self.parts:
228225
return (None, self.resolve(data))
229226

230-
if isinstance(data, str):
231-
_data = json.loads(data)
232-
elif isinstance(data, IOBase):
233-
_data = json.loads(data.read())
234-
else:
235-
_data = data
236-
227+
_data = _load_data(data)
237228
parent = reduce(self._getitem, self.parts[:-1], _data)
238229

239230
try:
@@ -603,6 +594,19 @@ def to(
603594
)
604595

605596

597+
def _load_data(data: Union[int, str, IOBase, Sequence[Any], Mapping[str, Any]]) -> Any:
598+
if isinstance(data, str):
599+
try:
600+
return json.loads(data)
601+
except json.JSONDecodeError:
602+
data = data.strip()
603+
if data.startswith('"') and data.endswith('"'):
604+
return data
605+
if isinstance(data, IOBase):
606+
return json.loads(data.read())
607+
return data
608+
609+
606610
def resolve(
607611
pointer: Union[str, Iterable[Union[str, int]]],
608612
data: Union[str, IOBase, Sequence[object], Mapping[str, object]],

jsonpath/selectors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
164164
)
165165
match.add_child(_match)
166166
yield _match
167-
elif isinstance(match.obj, Sequence):
167+
elif isinstance(match.obj, Sequence) and not isinstance(match.obj, str):
168168
norm_index = self._normalized_index(match.obj)
169169
with suppress(IndexError):
170170
_match = self.env.match_class(
@@ -195,7 +195,7 @@ async def resolve_async(
195195
)
196196
match.add_child(_match)
197197
yield _match
198-
elif isinstance(match.obj, Sequence):
198+
elif isinstance(match.obj, Sequence) and not isinstance(match.obj, str):
199199
norm_index = self._normalized_index(match.obj)
200200
with suppress(IndexError):
201201
_match = self.env.match_class(

tests/consensus.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class Query:
4545

4646
SKIP = {
4747
"bracket_notation_with_number_on_object": "Bad consensus",
48-
"bracket_notation_with_number_on_string": "Invalid document",
4948
"dot_notation_with_number_-1": "Unexpected token",
5049
"dot_notation_with_number_on_object": "conflict with compliance",
5150
}

0 commit comments

Comments
 (0)