Skip to content
Draft
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
31 changes: 24 additions & 7 deletions redisvl/schema/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import json
from functools import lru_cache
from typing import Any

from jsonpath_ng import parse as jsonpath_parse
Expand Down Expand Up @@ -210,6 +211,28 @@ def _validate_vector(cls, value):
return type(model_name, (BaseModel,), class_dict)


@lru_cache(maxsize=512)
def _compile_json_path(path: str) -> Any:
"""
Parse a JSONPath expression, reusing the result for repeated paths.

Schema field paths are fixed, and parsing costs ~1ms against microseconds to
evaluate, so validating per field re-did the expensive half every time.
Sharing one expression is safe: it holds no per-evaluation state.

Args:
path: JSONPath expression, with or without the leading ``$``

Returns:
The parsed jsonpath-ng expression
"""
# If path doesn't start with $, add it as per JSONPath spec
if not path.startswith("$"):
path = f"$.{path}"

return jsonpath_parse(path)


def extract_from_json_path(obj: dict[str, Any], path: str) -> Any:
"""
Extract a value from a nested JSON object using a JSONPath expression.
Expand All @@ -226,13 +249,7 @@ def extract_from_json_path(obj: dict[str, Any], path: str) -> Any:
and supports the full JSONPath specification including filters, wildcards,
and array indexing.
"""
# If path doesn't start with $, add it as per JSONPath spec
if not path.startswith("$"):
path = f"$.{path}"

# Parse and find the JSONPath expression
jsonpath_expr = jsonpath_parse(path)
matches = jsonpath_expr.find(obj)
matches = _compile_json_path(path).find(obj)

# Return the first match value, or None if no matches
if matches:
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
import pytest

from redisvl.schema import IndexSchema
from redisvl.schema import validation as validation_module
from redisvl.schema.fields import FieldTypes, VectorDataType
from redisvl.schema.schema import StorageType
from redisvl.schema.type_utils import TypeInferrer
from redisvl.schema.validation import (
SchemaModelGenerator,
_compile_json_path,
extract_from_json_path,
validate_object,
)
Expand Down Expand Up @@ -301,6 +303,51 @@ def test_extract_from_json_path(self, valid_json_data, path, expected_value):
"""Test extracting values using JSON paths."""
assert extract_from_json_path(valid_json_data, path) == expected_value

@pytest.fixture
def parse_calls(self, monkeypatch):
"""Record every path handed to jsonpath-ng's parser."""
calls: list[str] = []
real_parse = validation_module.jsonpath_parse

def counting_parse(path):
calls.append(path)
return real_parse(path)

_compile_json_path.cache_clear()
monkeypatch.setattr(validation_module, "jsonpath_parse", counting_parse)
return calls

def test_repeated_path_compiles_once(self, valid_json_data, parse_calls):
"""A repeated path compiles once, not once per call."""
for _ in range(25):
assert (
extract_from_json_path(valid_json_data, "$.metadata.user") == "user123"
)

assert parse_calls == ["$.metadata.user"]

def test_distinct_paths_each_compile(self, valid_json_data, parse_calls):
"""Caching must not collapse distinct paths onto one expression."""
assert extract_from_json_path(valid_json_data, "$.metadata.user") == "user123"
assert extract_from_json_path(valid_json_data, "$.metadata.rating") == 4.5
assert len(parse_calls) == 2

def test_shared_expression_holds_no_state(self):
"""Reusing one expression must not leak values between evaluations."""
_compile_json_path.cache_clear()

for expected in ("alice", "bob", "carol"):
obj = {"metadata": {"user": expected}}
assert extract_from_json_path(obj, "$.metadata.user") == expected

assert extract_from_json_path({"metadata": {}}, "$.metadata.user") is None

def test_leading_dollar_is_optional(self, valid_json_data):
"""Both spellings of a path resolve to the same value."""
assert extract_from_json_path(
valid_json_data, "metadata.user"
) == extract_from_json_path(valid_json_data, "$.metadata.user")


# # -------------------- CATEGORY 2: PARAMETRIZED VALIDATOR TESTS --------------------

Expand Down
Loading