json-path / JsonPath

Java JsonPath implementation
Apache License 2.0
8.93k stars 1.64k forks source link

Filter expression return array can not use [index] get item inside #272

Open gyk001 opened 8 years ago

gyk001 commented 8 years ago

eg. I want get the price for book "Sayings of the Century"

👍 $.store.book[?(@.title=='Sayings of the Century')] will return an book array 👍 $.store.book[?(@.title=='Sayings of the Century')].price will return an price array

😂 $.store.book[?(@.title=='Sayings of the Century')][0] will return an empty array 😂 $.store.book[?(@.title=='Sayings of the Century')].price[0] will return an empty array

I think $.store.book[?(@.title=='Sayings of the Century')][0] should return a book $.store.book[?(@.title=='Sayings of the Century')].price[0] should return a price

kohlsalem commented 7 years ago

Somehow i have a similar issue here https://github.com/openhab/openhab/issues/4768

i there any way to address the item after a filtering has been done?

kohlsalem commented 7 years ago

I found out a possible solution:

$.store.book[?(@.title=='Sayings of the Century')].price.min()

would take the one value from the list...

kallestenflo commented 7 years ago

A path must point to something in the document. That is the case for:

$.store.book[?(@.title=='Sayings of the Century')]

but not with:

$.store.book[?(@.title=='Sayings of the Century')][0]

where the [0] actually is expected to be applied to the result of the path evaluation. I agree that this would useful in many situations but it should not be confused with the actual path.

apocheau commented 7 years ago

My 2 cents. This is a workaround using read method on the result of the filter:

String filterResult = JsonPath.read(fullJson, "$.store.book[?(@.title == 'Sayings of the Century')]").toJSONString(); Double price = JsonPath.read(filterResult, "$[0].price");

Hope it could help until we will be able to do sort of: $.store.book[?(@.title=='Sayings of the Century')][0].price

RamakrishnanArun commented 7 years ago

Any chance of getting a way to support this? Would be a nice to have.

kallestenflo commented 7 years ago

@jochenberger whats your thoughts on this?

jochenberger commented 7 years ago

That's a tough one. It's apparently not part of the original JsonPath spec and is not supported on any of the implementations. I have a similar use case in the project I use JsonPath for and I have decided to create helper methods findAll(object, path) and findFirst(object, path) where the latter calls JsonPath.parse(object).limit(1).read(path) and returns an appropriate response. I'd say we should stick to the spec and not support this, but it's not a very strong opinion.

RamakrishnanArun commented 7 years ago

I would agree that going off spec is not the best idea yes because then you have an excuse to just add anything even if it deviates spec. Maybe custom functions or some sort of extension capability which is separate from the base project (which is pure spec).

RamakrishnanArun commented 7 years ago

What are the contribution guidelines in terms of "accepting any terms" or processes. I thought I might try experimenting.

jochenberger commented 7 years ago

I don't think there are any terms. Adding tests is a good way to get PRs merged, so is not breaking existing ones. ;-)

243 seems related btw.

jochenberger commented 7 years ago

And there's also #191 and #197.

consultantleon commented 7 years ago

Struggeling with the same and @kallestenflo have a hard time to understand your argument:

A path must point to something in the document. That is the case for: $.store.book[?(@.title=='Sayings of the Century')] but not with: $.store.book[?(@.title=='Sayings of the Century')][0] where the [0] actually is expected to be applied to the result of the path evaluation. I agree that this would useful in many situations but it should not be confused with the actual path.

The result of the path operation after the filter is a JSONArray, so at that point he document is a JSON Array, e.g. with 1 element. So now this is a new document and [0] points into the first element of this new intermediate document.

Let's study further based on my current real world example, parsing cloud foundry environment information.

Realworld example, a typical vcap_service cloudfoundry env variable value: { "mysql56": [ { "credentials": { "dbname": "asfawrwer", "hostname": "10.11.12.133", "password": "awerawerwerweaar", "port": "44444", "ports": { "3306/tcp": "55555" }, "uri": "mysql://awrwefawefaewr:awerawerwerweaar@10.11.12.133:55555/asfawrwer", "username": "awrwefawefaewr" }, "label": "mysql56", "name": "my-persistence", "plan": "free", "tags": [ "mysql56" ] } ] }

And we need to access the credentials, e.g. the hostname: $.*[?(@.name == 'my-persistence')].credentials.hostname

Currently this returns a JSONArray of 1 element so I tried: $.*[?(@.name == 'my-persistence')][0].credentials.hostname

and

$.*[?(@.name == 'my-persistence')].credentials.hostname[0]

to get a clean String value returned, but no luck due to this issue.

In my view after: $.*[?(@.name == 'my-persistence')]

The 'intermediate document' that the next operator is applied to is: [ { "credentials": { "dbname": "asfawrwer", "hostname": "10.11.12.133", "password": "awerawerwerweaar", "port": "44444", "ports": { "3306/tcp": "55555" }, "uri": "mysql://awrwefawefaewr:awerawerwerweaar@10.11.12.133:55555/asfawrwer", "username": "awrwefawefaewr" }, "label": "mysql56", "name": "my-persistence", "plan": "free", "tags": [ "mysql56" ] } ]

(because it is valid to access $.*[?(@.name == 'my-persistence')].credentials.hostname which returns [ "10.11.12.133" ] )

So why not allow the path to navigate to [0], after which the intermediate document is: { "credentials": { "dbname": "asfawrwer", "hostname": "10.11.12.133", "password": "awerawerwerweaar", "port": "44444", "ports": { "3306/tcp": "55555" }, "uri": "mysql://awrwefawefaewr:awerawerwerweaar@10.11.12.133:55555/asfawrwer", "username": "awrwefawefaewr" }, "label": "mysql56", "name": "my-persistence", "plan": "free", "tags": [ "mysql56" ] }

then naviate to credentials.hostname and get a clean string value "10.11.12.133" ?

jhlweb commented 7 years ago

@jochenberger and @kallestenflo is there any support on this. Filtering should not lead to a non accessible array.
The problem is getting old and a solution is not found yet in which this is handled within the JSONPATH call itself without additional script functions

keetron commented 6 years ago

Good to see I am not the only one struggling with this. I would expect such a filter to return whatever the content type is, not forced in a single result array.

My workaround is to parse the one result to a string and either add the following to the assert somewhere: expectedResult = "[\"" + expectedResult + "\"]" or strip the last and first two characters from the String returned but somehow I feel that is worse.

Is it because you stay with the content type list and filter inside that? In which case I would have to say I see why you went with a single entry list and I will alter my approach to use something like what I found at @fhoeben 's commit and use result.get(0)

Yeah, it all makes a lot more sense now.

Still would like it to return the object type of the actual object referenced to.

pahaderajesh commented 6 years ago

How to get he array name?

My json data is as below { "store": { "book": [ { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 }, { "category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99 }, { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 }, { "category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99 } ], "bicycle": { "color": "red", "price": 19.95 } }, "expensive": 10 }

I am looking for list of store only i.e. in above case output should be just

"book" "bicycle"

For this what should be json path . Also would like to apply the filter init as price should be greater than 1.

GrayedFox commented 6 years ago

I know that this is not the same as being able to select any given element, but I think a lot of people end up here because they are looking for a way to select the first (or maybe last) element of the resulting array from an applied filter. Therefore I don't think we should necessarily go for:

$.store.book[?(@.title=='Sayings of the Century')][0]

And expect a specific element of the array, because square brackets, for JSON path, is either square bracket notation or applying a filter (and there's nothing in the spec that specifically covers this use case where we want to mix both).

I would argue the closest way to adhere to the spec is to add some more methods, called after the filter, the same way we do for .min() and .max() - except they are not tied to the values of the array, but instead the elements, namely: .first() and .last().

It's not as powerful but could be easier to implement and resolve a number of people's issues?

CMoH commented 6 years ago

I'm having problems understanding how come a filtered array is not an array, that is without any knowledge of library internals.

gsambasiva commented 6 years ago

Any update on this issue?

nanonull commented 6 years ago

Hey! What is workaround to get this work in a single path selector?

DinoChiesa commented 5 years ago

Apparently there is no workaround in a single path selector. The workaround is to read in 2 stages.

zakjan commented 5 years ago

I had to build my own parser because of this issue, it was surprisingly easy with ANTLR4

fhoeben commented 5 years ago

@zakjan any chance you published that parser? Maybe others could benefit also.

zakjan commented 5 years ago

@fhoeben Yeah, I'll try to extract it and share

bhreinb commented 5 years ago

Hi @zakjan I'd be interested in seeing this too if you don't mind sharing? Thanks in advance.

gideonaina commented 5 years ago

I like @apocheau workaround suggestion (it less hacky) but I think this might also be a good one:

List<Double> filterResult = JsonPath.read(fullJson, "$.store.book[?(@.title == 'Sayings of the Century')].price");

then

Double price = filterResult.get(0);

It eliminates all the toString() methods.

AntonioDell commented 4 years ago

My workaround for kotlin applications is to extend DocumentContext with a function to read a String directly, like this:

fun DocumentContext.readString(path: String): String {
    val values = this.read<List<String>>(path)
    return if (values.isNotEmpty()) values.first() else ""
}

Then it can be used like this:

val singleValue: String = JsonPath.parse(myJsonString).readString("$.properties[?(@.key=='SampleKey')].values")

EDIT: Improved to handle empty results EDIT 2: Use right example for a JsonPath expression which would result in a string list with only one entry.

zakjan commented 4 years ago

@fhoeben @bhreinb Sorry for late response. My parser is already published at https://github.com/zakjan/objectpath . It supports more advanced cases, might be too complex for general use cases. Feel free to use it as a reference for building your own parser.

okainov commented 3 years ago

Almost 5 years people are struggling with it and unfortunately no any progress here :( Too sad :(

mredeker commented 3 years ago

This is really an issue for us also. We migrate a project from an older version of the library "json-path-0.8.1.jar" to "json-path-2.4.0.jar". In 0.8.1 the result was not wrapped in an [ ]. Why is it now??

gauravphoenix commented 3 years ago

I would argue the closest way to adhere to the spec is to add some more methods, called after the filter, the same way we do for .min() and .max() - except they are not tied to the values of the array, but instead the elements, namely: .first() and .last().

It's not as powerful but could be easier to implement and resolve a number of people's issues?

Of all the solutions mentioned, I like this solution the best as it doesn't break the original spec

sskorupski commented 3 years ago

Almost 5 years people are struggling with it and unfortunately no any progress here :( Too sad :(

I moved to

    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
    </dependency>

With this dependency I'm able to use a json path like $.energies[?(@.type == 'Electric')].level[0] which return the first element as needed here a sample usage:

var jsonPath = JSONPath.compile(path);
result = "";
if (jsonPath.contains(jsonResponse)) {
   result = jsonPath.eval(jsonResponse).toString();
}
petersunbag commented 2 years ago

I have a simple workaround. The idea is to make the jsonPath multiple pieces and run JsonPath.read multiple times. The code in kotlin version is below.

    private fun jsonPathRead(json: Any, jsonPath: String) : Any? {
        var currentObj = json
        jsonPath.replace("[", ";$[").split(';').forEach {
            currentObj = JsonPath.read(currentObj, it)
        }
        return currentObj
    }

For example, $.store.book[?(@.title=='Sayings of the Century')][0] becomes $.store.book;$[?(@.title=='Sayings of the Century')];$[0] and runs JsonPath.read 3 times JsonPath.read(currentObj, "$.store.book") JsonPath.read(currentObj, "$[?(@.title=='Sayings of the Century')]") JsonPath.read(currentObj, "$[0]")

genezx commented 2 years ago

just a suggestion on the jsonpath syntax: $.store.book[?(@.title=='Sayings of the Century')] will return a book array $.store.book[?(@.title=='Sayings of the Century')[0]] will return the first book item $.store.book[?(@.title=='Sayings of the Century')].price will return a price array $.store.book[?(@.title=='Sayings of the Century')[0]].price will return the first price it should not break compatibility to existing syntax, for some implementations that assume $...arr[....] will always return one member of arr.

v-mwalk commented 1 year ago

I've just come across this issue as well. I'd assumed that JSONPath was the JSON version of XPath for XML documents.

The not being able to index the result of a filter is quite a pain. This ticket has been open for 7 years now. Any prospect of it happening?

Klaas68 commented 1 year ago

I encountered exactly the same issue as discussed on this page. Because the order in my response array under test is pretty complex, I want to test the individual entries for the correct values baased on the unique "token" field of each response array element. Of course, after filtering I expect a single element (if zero or more are found, the test should fail) and then verify some fields based on this single element. But as discussed in this post, the filter actually results in a single-element array. There is no way to access this by using [0] or a firstElement function. I solved it by putting the value tot test against also in an array using the Java List.of() function.. It does not look very nice but is straightforward and does not need any scripts or custom functions:

Result after filtering:

[
  {
    "token": "6064364892108791641",
    "productId": 390403,
    "importStatus": "SUCCESS",
    "cardPrintDate": "2022-10-05T21:46:07.270792",
    "expirationDate": null,
    "importErrorCode": "",
    "importErrorMessage": ""
  }
]

And in the unit test: .andExpect(jsonPath("$[?(@.token == '6064364892108791641')].importStatus", equalTo(List.of("SUCCESS"))))

And this works for now.

ivan-kleshnin commented 6 months ago

I would argue the closest way to adhere to the spec is to add some more methods, called after the filter, the same way we do for .min() and .max() - except they are not tied to the values of the array, but instead the elements, namely: .first() and .last(). It's not as powerful but could be easier to implement and resolve a number of people's issues?

It would not help to paginate the array. Custom functions would make a bad palliative, there's no reason why arrays from filter expressions should behave differently from other arrays.

zhavir commented 5 months ago

did you find any workaround?

amiduai commented 2 months ago

This issue is still open. 8 years, incredible!