Open gyk001 opened 8 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?
I found out a possible solution:
$.store.book[?(@.title=='Sayings of the Century')].price.min()
would take the one value from the list...
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.
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
Any chance of getting a way to support this? Would be a nice to have.
@jochenberger whats your thoughts on this?
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.
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).
What are the contribution guidelines in terms of "accepting any terms" or processes. I thought I might try experimenting.
I don't think there are any terms. Adding tests is a good way to get PRs merged, so is not breaking existing ones. ;-)
And there's also #191 and #197.
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" ?
@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
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.
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.
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?
I'm having problems understanding how come a filtered array is not an array, that is without any knowledge of library internals.
Any update on this issue?
Hey! What is workaround to get this work in a single path selector?
Apparently there is no workaround in a single path selector. The workaround is to read in 2 stages.
I had to build my own parser because of this issue, it was surprisingly easy with ANTLR4
@zakjan any chance you published that parser? Maybe others could benefit also.
@fhoeben Yeah, I'll try to extract it and share
Hi @zakjan I'd be interested in seeing this too if you don't mind sharing? Thanks in advance.
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.
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.
@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.
Almost 5 years people are struggling with it and unfortunately no any progress here :( Too sad :(
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??
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
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();
}
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]")
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.
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?
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.
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.
did you find any workaround?
This issue is still open. 8 years, incredible!
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 arrayI 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