Skip to content

JsonPath AST and matching#124

Closed
simbo1905 wants to merge 5 commits intomainfrom
cursor/jsonpath-ast-and-matching-cc49
Closed

JsonPath AST and matching#124
simbo1905 wants to merge 5 commits intomainfrom
cursor/jsonpath-ast-and-matching-cc49

Conversation

@simbo1905
Copy link
Owner

What changed

A new Maven module json-java21-jsonpath has been added, providing a JsonPath query engine for the java.util.json Java 21 backport.

This module includes:

  • A recursive descent parser to convert JsonPath expressions into a type-safe AST (Abstract Syntax Tree) using Java 21 records and sealed interfaces.
  • An evaluator that matches the parsed AST against JsonValue documents from the core library.
  • Comprehensive test coverage based on all examples from the official JsonPath specification by Stefan Goessner.
  • New AGENTS.md and ARCHITECTURE.md files for the module.

Why this change is needed

This change addresses the need for a native JsonPath implementation within the java.util.json ecosystem. The primary motivations were:

  • To provide JsonPath querying capabilities without introducing external dependencies, adhering to a java.base only constraint.
  • To leverage modern Java 21 features (records, sealed interfaces, pattern matching) for a robust and functional design.
  • To follow a pure TDD approach, ensuring high quality and correctness, with all Goessner examples serving as unit tests.
  • To align with existing project patterns, similar to the JTD module, for operating on the core java.util.json API.

How were these changes tested

The changes were developed using a Pure TDD approach.

  • Unit Tests: JsonPathAstTest (17 tests) verifies the correctness of the AST structure. JsonPathParserTest (32 tests) ensures the parser correctly converts various JsonPath expressions into the expected AST.
  • Integration Tests: JsonPathGoessnerTest (27 tests) covers all examples from the official JsonPath specification by Stefan Goessner, validating the end-to-end query evaluation against a sample JSON document.
  • Build Verification: All tests pass, and the module successfully builds and integrates with the parent project.

Checklist

  • Code builds / passes tests
  • New tests added if needed
  • Update to use CODING_STYLE_LLM.md convensions
  • Documentation updated if needed
  • AGENTS.md updated if appropriate

Open in Cursor Open in Web

Implements JsonPath query language for the java.util.json backport based on
Stefan Goessner's specification (https://goessner.net/articles/JsonPath/).

Features:
- Parses JsonPath expressions to AST (sealed interface + records)
- Evaluates AST against JsonValue documents from core library
- No external dependencies (java.base only)
- Java 21 functional style (immutable records, pattern matching)
- Pure TDD development with 76 comprehensive tests

Supported operators:
- Root ($), property access (.name, ['name'])
- Array index ([n], [-1]), slice ([:2], [0:10:2])
- Wildcard ([*], .*), recursive descent (..)
- Union ([0,1], ['a','b'])
- Filter expressions ([?(@.isbn)], [?(@.price<10)])
- Script expressions ([(@.length-1)])

All examples from Goessner article implemented as tests.

To verify:
  mvn test -pl json-java21-jsonpath -Djava.util.logging.ConsoleHandler.level=INFO

Co-authored-by: simbo1905 <simbo1905@60hertz.com>
@cursor
Copy link

cursor bot commented Feb 1, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@gemini-code-assist
Copy link

Summary of Changes

Hello @simbo1905, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new capability by integrating a native JsonPath query engine into the java.util.json ecosystem. The primary goal is to offer powerful JSON querying functionalities without introducing external dependencies, leveraging the latest Java 21 language features for a clean and efficient implementation. The development process emphasized a pure Test-Driven Development approach, ensuring the correctness and reliability of the new functionality through rigorous testing against established specifications.

Highlights

  • New Module Introduction: A new Maven module, json-java21-jsonpath, has been added to provide a native JsonPath query engine for the java.util.json Java 21 backport.
  • AST-based Parsing: JsonPath expressions are now parsed into a type-safe Abstract Syntax Tree (AST) using modern Java 21 features like records and sealed interfaces, enabling robust and functional design.
  • Comprehensive Evaluation Engine: An evaluator has been implemented to match the parsed AST against JsonValue documents from the core library, supporting various JsonPath operators including property access, array indexing, slicing, wildcards, recursive descent, filters, unions, and script expressions.
  • Extensive Test Coverage: The implementation includes comprehensive test coverage, with unit tests for the AST and parser, and integration tests based on all examples from Stefan Goessner's official JsonPath specification.
  • Documentation: New AGENTS.md and ARCHITECTURE.md files have been added to detail the module's purpose, structure, design principles, supported features, and usage.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Adds alternative API style for JsonPath queries:
- JsonPath.parse(expr) returns a compiled JsonPath
- .select(json) evaluates against a document
- Compiled paths are reusable across multiple documents
- Retains original JsonPath.query(expr, json) as convenience method

Tests verify:
- parse(...).select(...) returns same results as query(...)
- Compiled paths can be reused on different documents
- expression() accessor returns original path string

To verify: mvn test -pl json-java21-jsonpath

Co-authored-by: simbo1905 <simbo1905@60hertz.com>
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 262dadaf32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +295 to +297
if (properties.isEmpty()) {
// Just @ with no properties
return new JsonPathAst.PropertyPath(List.of("@"));

Choose a reason for hiding this comment

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

P2 Badge Treat bare @ as current node in filters

The parser maps a bare @ to PropertyPath(List.of("@")), which makes filters like [?(@)] or comparisons such as [?(@ == 1)] resolve a property literally named @ instead of the current node. Since resolvePropertyPath only follows object members, these expressions will never match unless the JSON object actually has an "@" key. The AST already has CurrentNode, so this is a behavioral bug where valid JsonPath filters against the current node evaluate as false.

Useful? React with 👍 / 👎.

Comment on lines +247 to +249
default -> {
// Other segment types in recursive descent context
LOG.finer(() -> "Unsupported target in recursive descent: " + target);

Choose a reason for hiding this comment

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

P2 Badge Recursive descent ignores bracket targets like unions

The recursive descent evaluator only handles PropertyAccess, Wildcard, and ArrayIndex targets, and falls through to the default case for others. However, the parser allows recursive descent with bracket notation (e.g. $..['author','title'] or $..[?(@.price<10)]), producing Union, Filter, ArraySlice, or ScriptExpression targets. These are silently ignored here, so those valid expressions yield no matches. Either the parser should reject those forms or evaluateTargetSegment should support them.

Useful? React with 👍 / 👎.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new json-java21-jsonpath module, which is a significant and well-structured addition. The use of modern Java features like records and sealed interfaces for the AST is excellent, and the TDD approach with comprehensive tests against the Goessner specification provides a solid foundation. The documentation in AGENTS.md and ARCHITECTURE.md is thorough and very helpful.

My review has identified a few issues, primarily related to edge cases and completeness in the parser and evaluator. Specifically, I've found areas for improvement in handling recursive descent targets, parsing the current node selector (@), evaluating literal values in filters, and a discrepancy between documented and implemented support for logical operators in filters. Addressing these points will enhance the robustness and correctness of this new JsonPath engine.

Comment on lines +209 to +252
if (current instanceof JsonObject obj) {
for (final var value : obj.members().values()) {
evaluateSegments(segments, index + 1, value, root, results);
}
} else if (current instanceof JsonArray array) {
for (final var element : array.elements()) {
evaluateSegments(segments, index + 1, element, root, results);
}
}
}

private static void evaluateRecursiveDescent(
JsonPathAst.RecursiveDescent desc,
List<JsonPathAst.Segment> segments,
int index,
JsonValue current,
JsonValue root,
List<JsonValue> results) {

// First, try matching the target at current level
evaluateTargetSegment(desc.target(), segments, index, current, root, results);

// Then recurse into children
if (current instanceof JsonObject obj) {
for (final var value : obj.members().values()) {
evaluateRecursiveDescent(desc, segments, index, value, root, results);
}
} else if (current instanceof JsonArray array) {
for (final var element : array.elements()) {
evaluateRecursiveDescent(desc, segments, index, element, root, results);
}
}
}

private static void evaluateTargetSegment(
JsonPathAst.Segment target,
List<JsonPathAst.Segment> segments,
int index,
JsonValue current,
JsonValue root,
List<JsonValue> results) {

switch (target) {
case JsonPathAst.PropertyAccess prop -> {

Choose a reason for hiding this comment

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

high

The evaluateTargetSegment method is incomplete. It only handles PropertyAccess, Wildcard, and ArrayIndex as targets for a recursive descent (..). Other valid segments like Filter, ArraySlice, etc., are not supported. This will cause queries like $..[?(@.price<10)] to fail silently, as the default case in the switch statement just logs a message without performing any evaluation.

To fix this, you should add cases for the other Segment types. This will likely introduce some code duplication from the main evaluate* methods, which could be addressed in a separate refactoring. I'd also recommend adding a test case for a path like $..[?(@.price<10)] to verify the fix.

Comment on lines +271 to +301
private JsonPathAst.PropertyPath parseCurrentNodePath() {
pos++; // skip @

final var properties = new ArrayList<String>();

while (pos < path.length() && path.charAt(pos) == '.') {
pos++; // skip .
final int start = pos;

while (pos < path.length()) {
final char c = path.charAt(pos);
if (!Character.isLetterOrDigit(c) && c != '_') {
break;
}
pos++;
}

if (pos == start) {
throw new JsonPathParseException("Expected property name after '.'", path, pos);
}

properties.add(path.substring(start, pos));
}

if (properties.isEmpty()) {
// Just @ with no properties
return new JsonPathAst.PropertyPath(List.of("@"));
}

return new JsonPathAst.PropertyPath(properties);
}

Choose a reason for hiding this comment

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

high

The parser incorrectly represents the current node selector @ as a PropertyPath with the property name "@". This causes incorrect behavior during filter evaluation, as the evaluator will then try to find a property named "@" instead of using the current context node.

The fix is to modify parseCurrentNodePath to return a JsonPathAst.CurrentNode when it parses a standalone @. This will require changing the method's return type to the more general JsonPathAst.FilterExpression.

    private JsonPathAst.FilterExpression parseCurrentNodePath() {
        pos++; // skip @

        if (pos >= path.length() || path.charAt(pos) != '.') {
            return new JsonPathAst.CurrentNode();
        }

        final var properties = new ArrayList<String>();

        while (pos < path.length() && path.charAt(pos) == '.') {
            pos++; // skip .
            final int start = pos;

            while (pos < path.length()) {
                final char c = path.charAt(pos);
                if (!Character.isLetterOrDigit(c) && c != '_') {
                    break;
                }
                pos++;
            }

            if (pos == start) {
                throw new JsonPathParseException("Expected property name after '.'", path, pos);
            }

            properties.add(path.substring(start, pos));
        }

        return new JsonPathAst.PropertyPath(properties);
    }

<version>0.1.9</version>
</parent>

<artifactId>java.util.json.jsonpath</artifactId>

Choose a reason for hiding this comment

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

medium

The artifactId java.util.json.jsonpath is inconsistent with the module name json-java21-jsonpath defined in the parent pom.xml and the project's directory structure. For better project clarity, consistency, and ease of navigation, it's recommended to align the artifactId with the module name.

Suggested change
<artifactId>java.util.json.jsonpath</artifactId>
<artifactId>json-java21-jsonpath</artifactId>

JsonPathAst.Filter filter,
List<JsonPathAst.Segment> segments,
int index,
JsonValue current,

Choose a reason for hiding this comment

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

medium

The matchesFilter method currently returns true for any LiteralValue. This means a filter expression like [?(false)] or [?(0)] would incorrectly match all elements. While the JSONPath specification is not explicit about literal truthiness, this behavior is likely unintended and can lead to incorrect query results.

A safer implementation would be to handle boolean literals correctly and either throw an exception or return false for other literal types.

            case JsonPathAst.LiteralValue lv -> {
                if (lv.value() instanceof Boolean b) {
                    yield b;
                }
                // Non-boolean literals in a filter context don't have a clear
                // "truthy" definition in JSONPath. Returning false is safer than true.
                yield false;
            }

Comment on lines +202 to +226
private JsonPathAst.FilterExpression parseFilterContent() {
if (pos >= path.length()) {
throw new JsonPathParseException("Unexpected end of filter expression", path, pos);
}

// Parse the left side (usually @.property)
final var left = parseFilterAtom();

skipWhitespace();

// Check if there's a comparison operator
if (pos < path.length() && path.charAt(pos) != ')') {
final var op = parseComparisonOp();
skipWhitespace();
final var right = parseFilterAtom();
return new JsonPathAst.ComparisonFilter(left, op, right);
}

// No operator means existence check
if (left instanceof JsonPathAst.PropertyPath pp) {
return new JsonPathAst.ExistsFilter(pp);
}

throw new JsonPathParseException("Invalid filter expression - expected property path", path, pos);
}

Choose a reason for hiding this comment

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

medium

The parseFilterContent method currently only parses a single filter condition (either an existence check or a single comparison). It does not support logical operators like && and || to combine multiple conditions, e.g., [?(@.price < 10 && @.isbn)].

However, the AST (LogicalFilter) and evaluator (matchesFilter) include support for these operators, and ARCHITECTURE.md mentions them as "implemented but not extensively tested".

To align the implementation with the documentation, the parser should be enhanced to handle logical expressions, including operator precedence. If supporting logical operators is out of scope for now, the documentation in ARCHITECTURE.md should be updated to reflect that they are not yet implemented in the parser.

@simbo1905 simbo1905 closed this Feb 1, 2026
@simbo1905 simbo1905 deleted the cursor/jsonpath-ast-and-matching-cc49 branch February 1, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants