10

Given JSON structured like this:

{
   "name":"Some Guy",
   "emails":[
      {
         "description":"primary",
         "status":"UNVERIFIED",
         "email":"first@first-email.com"
      },
      {
         "description":"home",
         "status":"VERIFIED",
         "email":"second@second-email.com"
      },
      {
         "description":"away",
         "status":"VERIFIED",
         "email":"third@third-email.com"
      }
   ]
}

I would like a JSONPath expression to get the first email with status VERIFIED and if there are none, then just get the first email in the array. So, given the example above, the result would be second@second-email.com. Given this example:

{
   "name":"Some Guy",
   "emails":[
      {
         "description":"primary",
         "status":"UNVERIFIED",
         "email":"first@first-email.com"
      },
      {
         "description":"home",
         "status":"UNVERIFIED",
         "email":"second@second-email.com"
      }
   ]
}

the result would be first@first-email.com.

Is this possible with a JSONPath expression?

approxiblue
  • 6,982
  • 16
  • 51
  • 59
jhericks
  • 5,833
  • 6
  • 40
  • 60
  • Which JSONPath implementation are you using? Do you need to do this in one expression? – approxiblue Jan 09 '17 at 21:47
  • Today I am using the jayway Java implementation, but if you have other implementations that would work I could figure out how to integrate that implementation. I had in mind doing it in a single expression yes, but if you had a way where it could be expressed as a chain of expressions I'd be open to considering that. It just can't require my own Java or Javascript code to process intermediate results. – jhericks Jan 09 '17 at 21:52

2 Answers2

16

You effectively have 2 JSONPath expressions, where the second one (first email) should be evaluated only if the first one (first verified email) returns nothing, so I don't think you can evaluate them both at the same time, in a single expression.

You can apply them one after the other, though:

public static String getEmail(String json) {
    Configuration cf = Configuration.builder().options(Option.SUPPRESS_EXCEPTIONS).build();
    DocumentContext ctx = JsonPath.using(cf).parse(json);
    List<String> emails = ctx.read("$.emails[?(@.status == 'VERIFIED')].email");
    if (!emails.isEmpty()) {
        return emails.get(0);
    }
    return ctx.read("$.emails[0].email");
}

If the email array is empty, ctx.read("$.emails[0].email") will return null instead of throwing an exception, thanks to the option SUPPRESS_EXCEPTIONS.


If you don't know the number of paths in advance:

public static String getEmail(String json, String[] paths) {
    Configuration cf = Configuration.builder().options(Option.ALWAYS_RETURN_LIST, Option.SUPPRESS_EXCEPTIONS).build();
    DocumentContext ctx = JsonPath.using(cf).parse(json);
    for (String path : paths) {
        List<String> emails = ctx.read(path);
        if (!emails.isEmpty()) {
            return emails.get(0);
        }
    }
    return null;
}

The option ALWAYS_RETURN_LIST means the return type is a list, even when you have one or zero results.

approxiblue
  • 6,982
  • 16
  • 51
  • 59
  • The issue is that I just have some JSON input and a map keys to JSONPath expressions. I don't know what the expressions are up front - they are configurable, so having something like this `getEmail` method is not going to work for me. – jhericks Jan 09 '17 at 23:57
  • @jhericks The method is more for demonstration. You can apply those 2 expressions one after the other, so you can turn both of the hard-coded expressions in my example into arguments. – approxiblue Jan 10 '17 at 00:01
  • Say I had an array of expressions: `["$.emails[?(@.status == 'VERIFIED')].email", "$.emails[0].email"]` What would a method look like that would just iterate through these expressions and give the correct email? – jhericks Jan 10 '17 at 00:54
  • @jhericks you use `ALWAYS_RETURN_LIST` to make sure all reads return a list type, then loop through the paths. Answer updated. – approxiblue Jan 10 '17 at 01:06
  • That still doesn't quite do it for me, because we have to support things like the simple JSONPath: `$.emails` in which case, they would _want_ the list returned, but this gives me enough to go on. This is not the answer I was hoping for, but if no one comes up with anything superior in the next few days, I will accept your answer and award the bounty. – jhericks Jan 10 '17 at 01:49
  • Actually, I do have a follow-up. Is there a way to modify the `$.emails[?(@.status == 'VERIFIED')].email` expression itself to just return the first match (rather than a list)? I know `$.emails[?(@.status == 'VERIFIED')].email[0]` doesn't work but that's the kind of thing I'm looking for. – jhericks Jan 10 '17 at 01:54
  • @jhericks No, unfortunately. You can't combine a "where" clause and an index access. You can modify `getEmail` to always return a list and decide later on if you want to pick only the first one. – approxiblue Jan 10 '17 at 03:14
  • 1
    @jhericks For combining a filter with an index access, there's an open issue: https://github.com/jayway/JsonPath/issues/272 – approxiblue Jan 12 '17 at 06:24
-1

This code should work perfectly for you

//Use the json parsing library to extract the data
    JsonPath jp = new JsonPath(json); 
    Map<String, Object> location = jp.get("name.emails[0]");
    System.out.println(location);
  • This doesn't take the email.status into account. – jhericks Oct 05 '17 at 15:59
  • It shoul do. All the parameters of the first record will be captured. Print 'location' to console to verify –  Oct 05 '17 at 21:34
  • Another problem here is that you cannot assume the ordering of entries. A JSon array resembles to a JSon unordered list. – Jaco Van Niekerk Oct 06 '17 at 08:29
  • I don't mean that 'status' wouldn't be included. I mean it would just always get the first email without regard for whether status == 'VERIFIED' or not. Would it give a different result for the two JSON examples given? – jhericks Oct 13 '17 at 18:42