cburgmer/json-path-comparison

Support for path axis navigation

Closed this issue · 11 comments

Having just shopped around for a JSONPath implementation (javascript) to satisfy my needs I have noted the lack of axis navigation techniques available in most implementations. jsonpath-plus at least has the ^ operator which allows you to backtrack across parent objects once you have found a match. This feature stands out to me as very important, and completely overlooked by this survey :)

XPath has a whole array of axes you can navigate across. I'm not sure how useful most of them are, but i'm sure they have situations where they are indispensible. See https://developer.mozilla.org/en-US/docs/Web/XPath/Axes I find ancestor axis can be very useful for specific cases though.

My main use case for the parent operator is this:

Say I want to get the books that have bookmarks in them for page 45? I don't really care about the bookmark itself, but the book object! This is impossible with 'Proposal A'

expression: $[*].bookmarks[?(@.page == 45)]^^^ => [{"category": "fiction",... "bookmarks": [{"page": 35,"owner": "bob"},...]}]

[
    {
      "category": "reference",
      "author": "Nigel Rees",
      "title": "Sayings of the Century",
      "price": 8.95,
      "bookmarks": [{
          "page": 40,
          "owner": "bob"
      }]
    },
    {
      "category": "fiction",
      "author": "Evelyn Waugh",
      "title": "Sword of Honour",
      "price": 12.99,
      "bookmarks": [
        {
            "page": 35,
            "owner": "bob"
        },
        {
            "page": 45,
            "owner": "alice"
        }     
      ]
    },
    {
      "category": "fiction",
      "author": "Herman Melville",
      "title": "Moby Dick",
      "isbn": "0-553-21311-3",
      "price": 8.99,
      "bookmarks": [
        {
            "page": 3035,
            "owner": "chuck"
        },
        {
            "page": 435,
            "owner": "dennis"
        }     
      ]
    }
]

@jamie-pate, as the ^ operator is defined as an extension for at least one implementation in this comparison, I'd suggest submitting a pull request that covers this feature for @cburgmer's consideration.

Fork the repository and in the queries directory, create a new subdirectory with a name that follows the conventions, something like filter_expression_with_backtrack_operator. Include two files, document.json and selector, that contain the JSON document and JSONPath selector respectively.

By the way, you have one issue with your example, the name "bookmark" needs to be consistently singular or plural. In your selector and one member of your document, it is "bookmark", elsewhere in the document it is ""bookmarks".

I think your path has too many parent operators. Consider the output paths for each of these queries:

Query Output paths
$[*].bookmarks[?(@.page == 45)] "$[1]['bookmarks'][1]"
  "$[2]['bookmarks'][1]"
$[*].bookmarks[?(@.page == 45)]^ "$[1]['bookmarks']"
  "$[2]['bookmarks']"
$[*].bookmarks[?(@.page == 45)]^^ "$[1]"
  "$[2]"
$[*].bookmarks[?(@.page == 45)]^^^ "$"
  "$"

It seems to me that

$[*].bookmarks[?(@.page == 45)]^^

should produce your expected result. I think

$[*].bookmarks[?(@.page == 45)]^^^

should produce two complete original documents.

Daniel

It does seem that way but it turns out that ^^ only selects the bookmarks array.
jsonpath-plus seems to select ^ by the path, rather than the object that's matched.
$[*].bookmarks[?(@.page == 45)] is broken down to path parts
$, *, bookmarks, ?(@.page == 45)
So when you backtrack with ^^^ you get the * and that's what's returned (but filtered by the other path selectors)

See https://stackblitz.com/edit/js-9zmaib

Interesting. I don't understand what jsonpath-plus is doing here. If I modify your query to include the option resultType: "path", I get the output paths I'd expect:

query=$[*].bookmarks[?(@.page == 45)]
  path = $,*,bookmarks,?(@.page == 45)
  [
 "$[1]['bookmarks'][1]",
 "$[2]['bookmarks'][1]"
]

  query=$[*].bookmarks[?(@.page == 45)]^
  path = $,*,bookmarks,?(@.page == 45),^
  [
 "$[1]['bookmarks']",
 "$[2]['bookmarks']"
]

  query=$[*].bookmarks[?(@.page == 45)]^^
  path = $,*,bookmarks,?(@.page == 45),^,^
  [
 "$[1]",
 "$[2]"
]

  query=$[*].bookmarks[?(@.page == 45)]^^^
  path = $,*,bookmarks,?(@.page == 45),^,^,^
  [
 "$",
 "$"
]

Surely the returned output paths and the returned values for the same query should always be consistent?

Just to note, the first item in your doc

{
    ...
    "bookmark": [{
        "page": 40,
        "owner": "bob"
    }]
  }

still has "bookmark", should be "bookmarks". Also in pull request.

Ah, I corrected it in this issues instead 😳, PR updated now

One other thing I would suggest for the PR is to make the document smaller by removing some superfluous members, I would suggest removing category, author, isbn, and price from the book objects. It is enough to have title and the bookmark array. The general style in this project is to keep documents minimalistic.

Then it's up to @cburgmer.

Done

$ ./src/query_implementation.sh queries/filter_expression_with_parent_axis_operator ./implementations/JavaScript_jsonpath-plus
OK
[{"bookmarks": [{"page": 35}, {"page": 45}], "title": "Sword of Honour"}, {"bookmarks": [{"page": 3035}, {"page": 45}], "title": "Moby Dick"}]

Wow, thanks both of you for the thorough review.

PR has been merged, so I feel we can close this issue. Do reopen if there's more to discuss!