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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ These breaking changes apply to Python JSONPath in its default configuration. We

**JSONPath function extension changes**

- Added the `startswith(value, prefix)` function extension. `startswith` returns `True` if both arguments are strings and the second argument is a prefix of the first argument. See the [filter functions](https://jg-rp.github.io/python-jsonpath/functions/#startswith) documentation.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be implemented with regex, so this is for performance?

Copy link
Copy Markdown
Owner Author

@jg-rp jg-rp Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @He-Pin,

I have a use case where operators like and and or are preferable over && and ||, for example. Adding functions like startswith() where match() would do fits in well in that particular scenario.

Maybe non-standard function extension should be disabled in the new "strict" JSONPath environment 🤔

Copy link
Copy Markdown

@He-Pin He-Pin Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have something like this too, like isChiness to filter out Chinese text when we do TaoBao English.

Thanks for sharing

- The non-standard `keys()` function extension has been reimplemented. It used to be a simple Python function, `jsonpath.function_extensions.keys`. Now it is a "well-typed" class, `jsonpath.function_extensions.Keys`. See the [filter functions](https://jg-rp.github.io/python-jsonpath/functions/#keys) documentation.

**JSONPath features**
Expand Down
14 changes: 14 additions & 0 deletions docs/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ If _pattern_ is a string literal, it will be compiled at compile time, and raise

If _pattern_ is a query and the result is not a valid regex, `False` is returned.

## `startswith()`

**_New in version 2.0.0_**

```
startswith(value: str, prefix: str) -> bool
```

Return `True` if `value` starts with `prefix`. If `value` or `prefix` are not strings, `False` is returned.

```
$[?startswith(@, 'ab')]
```

## `typeof()`

**_New in version 0.6.0_**
Expand Down
1 change: 1 addition & 0 deletions jsonpath/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ def setup_function_extensions(self) -> None:
self.function_extensions["is"] = self.function_extensions["isinstance"]
self.function_extensions["typeof"] = function_extensions.TypeOf()
self.function_extensions["type"] = self.function_extensions["typeof"]
self.function_extensions["startswith"] = function_extensions.StartsWith()

def validate_function_extension_signature(
self, token: Token, args: List[Any]
Expand Down
2 changes: 2 additions & 0 deletions jsonpath/function_extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .length import Length
from .match import Match
from .search import Search
from .starts_with import StartsWith
from .typeof import TypeOf
from .value import Value

Expand All @@ -20,6 +21,7 @@
"Length",
"Match",
"Search",
"StartsWith",
"TypeOf",
"validate",
"Value",
Expand Down
21 changes: 21 additions & 0 deletions jsonpath/function_extensions/starts_with.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""The `startswith` function extension."""

from jsonpath.function_extensions import ExpressionType
from jsonpath.function_extensions import FilterFunction


class StartsWith(FilterFunction):
"""The `startswith` function extension."""

arg_types = [ExpressionType.VALUE, ExpressionType.VALUE]
return_type = ExpressionType.LOGICAL

def __call__(self, value: object, prefix: object) -> bool:
"""Return `True` if `value` starts with `prefix`."""
if not isinstance(value, str) or not isinstance(prefix, str):
return False

try:
return value.startswith(prefix)
except AttributeError:
return False
2 changes: 0 additions & 2 deletions tests/test_keys_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ class Case:
want: Union[Sequence[Any], Mapping[str, Any]]


SOME_OBJECT = object()

TEST_CASES = [
Case(
description="value in keys of an object",
Expand Down
51 changes: 51 additions & 0 deletions tests/test_startswith_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import dataclasses
import operator
from typing import Any
from typing import Mapping
from typing import Sequence
from typing import Union

import pytest

from jsonpath import JSONPathEnvironment


@dataclasses.dataclass
class Case:
description: str
path: str
data: Union[Sequence[Any], Mapping[str, Any]]
want: Union[Sequence[Any], Mapping[str, Any]]


TEST_CASES = [
Case(
description="current value start with string",
path="$[?startswith(@, 'ab')]",
data={"x": "abc", "y": "abx", "z": "bcd", "-": "ab"},
want=["abc", "abx", "ab"],
),
Case(
description="current key start with string",
path="$[?startswith(#, 'ab')]",
data={"abc": 1, "abx": 2, "bcd": 3, "ab": 4},
want=[1, 2, 4],
),
Case(
description="value is not a string",
path="$[?startswith(@, 'ab')]",
data={"abc": 1, "abx": 2, "bcd": 3, "ab": 4},
want=[],
),
]


@pytest.fixture()
def env() -> JSONPathEnvironment:
return JSONPathEnvironment()


@pytest.mark.parametrize("case", TEST_CASES, ids=operator.attrgetter("description"))
def test_isinstance_function(env: JSONPathEnvironment, case: Case) -> None:
path = env.compile(case.path)
assert path.findall(case.data) == case.want
Loading