Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
gyk001 opened this issue Oct 18, 2016 · 38 comments
Open

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

gyk001 opened this issue Oct 18, 2016 · 38 comments
Labels

Comments

@gyk001
Copy link

gyk001 commented Oct 18, 2016

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
Copy link

Somehow i have a similar issue here openhab/openhab1-addons#4768

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

@kohlsalem
Copy link

I found out a possible solution:

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

would take the one value from the list...

@kallestenflo
Copy link
Contributor

kallestenflo commented Dec 1, 2016

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
Copy link

apocheau commented Feb 7, 2017

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
Copy link

RamakrishnanArun commented Mar 14, 2017

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

@kallestenflo
Copy link
Contributor

@jochenberger whats your thoughts on this?

@jochenberger
Copy link
Contributor

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
Copy link

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
Copy link

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

@jochenberger
Copy link
Contributor

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
Copy link
Contributor

And there's also #191 and #197.

@consultantleon
Copy link

consultantleon commented Aug 8, 2017

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
Copy link

jhlweb commented Sep 28, 2017

@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
Copy link

keetron commented Dec 14, 2017

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
Copy link

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
Copy link

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
Copy link

CMoH commented Oct 18, 2018

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

@gsambasiva
Copy link

Any update on this issue?

@nanonull
Copy link

nanonull commented Nov 1, 2018

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

@DinoChiesa
Copy link

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

@zakjan
Copy link

zakjan commented May 12, 2019

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

@fhoeben
Copy link

fhoeben commented May 12, 2019

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

@zakjan
Copy link

zakjan commented May 13, 2019

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

@bhreinb
Copy link

bhreinb commented May 28, 2019

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

@gideonaina
Copy link

gideonaina commented Jul 31, 2019

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
Copy link

AntonioDell commented Jun 23, 2020

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
Copy link

zakjan commented Jun 23, 2020

@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
Copy link

okainov commented Feb 18, 2021

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

@mredeker
Copy link

mredeker commented Mar 4, 2021

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
Copy link

gauravphoenix commented Jul 29, 2021

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
Copy link

sskorupski commented Oct 20, 2021

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
Copy link

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
Copy link

genezx commented Jun 30, 2022

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
Copy link

v-mwalk commented Jun 13, 2023

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
Copy link

Klaas68 commented Aug 16, 2023

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
Copy link

ivan-kleshnin commented Apr 12, 2024

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
Copy link

zhavir commented May 24, 2024

did you find any workaround?

@amiduai
Copy link

amiduai commented Aug 26, 2024

This issue is still open. 8 years, incredible!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests