9

Using a simple Json file e.g:

{"menu": {
  "id": "file",
  "value": "File",
  "popup": {
    "menuitem": [
      {"value": "New", "onclick": "CreateNewDoc()"},
      {"value": "Open", "onclick": "OpenDoc()"},
      {"value": "Close", "onclick": "CloseDoc()"}
    ]
  }
}}

I want to be able to get the JsonArray named menuitem using a path:

String path =  "menu.popup.menuitem"

I tried to do this using:

public static JsonElement fromString(String json, String path) throws JsonSyntaxException {
        JsonObject obj = GsonBuilder.create().fromJson(json, JsonObject.class);
        String[] seg = path.split(".");
        for (String element : seg) {
            if (obj != null) {
                obj = obj.get(element).getAsJsonObject();
            } else {
                return null;
            }
        }
        return obj
}

with:

JsonElement jsonElement = fromString(json, path);

But when I try isJsonArray() the return value is false. When doing the extra sanity check using Gson.toJson(jsonElement) the output is the full json String (above) that was inputted originally. What's going wrong?

isapir
  • 21,295
  • 13
  • 115
  • 116
Eduardo
  • 6,900
  • 17
  • 77
  • 121

2 Answers2

11

I'm not sure why this is not built-into Gson, but here is a method that I wrote, which returns a JsonElement given a JsonElement input and a JSON Path:

/**
 * Returns a JSON sub-element from the given JsonElement and the given path
 *
 * @param json - a Gson JsonElement
 * @param path - a JSON path, e.g. a.b.c[2].d
 * @return - a sub-element of json according to the given path
 */
public static JsonElement getJsonElement(JsonElement json, String path){

    String[] parts = path.split("\\.|\\[|\\]");
    JsonElement result = json;

    for (String key : parts) {

        key = key.trim();
        if (key.isEmpty())
            continue;

        if (result == null){
            result = JsonNull.INSTANCE;
            break;
        }

        if (result.isJsonObject()){
            result = ((JsonObject)result).get(key);
        }
        else if (result.isJsonArray()){
            int ix = Integer.valueOf(key) - 1;
            result = ((JsonArray)result).get(ix);
        }
        else break;
    }

    return result;
}

To call it, use something like:

String jsonString = ...;

Gson gson = new Gson();
JsonObject  jsonObject = gson.fromJson(jsonString, JsonObject.class);
JsonElement subElement = getJsonElement(jsonObject, "a.b.c[2].d";
isapir
  • 21,295
  • 13
  • 115
  • 116
  • 3
    It's not baked into Gson because there is a separate, more formal way of doing it called JSON Path. Here is one popular implementation https://github.com/json-path/JsonPath and here is the introduction to it https://www.baeldung.com/guide-to-jayway-jsonpath – Slawomir Apr 07 '19 at 19:49
  • You are missing a closing bracket in line 5. – ComanderKai77 Jul 15 '22 at 13:42
6

split uses regex to find places on which string should be split, but . in regex is special character which represents "any character beside line separators", which means that you are actually splitting on each character. So for string like

"foo"

"foo".split(".") will split on f, o, o

"foo"
 ^^^

which means you will get as result array with four empty strings (3 splits give 4 elements).

["", "", "", ""]

Actually I lied here because split(regex) does one additional thing: it removes trailing empty strings from result array, but your array contains only empty strings, which means that they will all be removed, so split(".") will return just empty array [] so your loop will not iterate even once (that is why your method returns unmodified obj).

To get rid of this problem you will need to make . literal (you need to escape it). To do so you can use for instance split("\\.") or split("[.]") or split(Pattern.quote(".") which work same as split("\\Q.\\E") - it adds quotation area.

Also inside loop you should first check type of Json you are handling, because getAsJsonObject will fail if Json is array. So your code should probably look like

public static JsonElement fromString(String json, String path)
        throws JsonSyntaxException {
    JsonObject obj = new GsonBuilder().create().fromJson(json, JsonObject.class);
    String[] seg = path.split("\\.");
    for (String element : seg) {
        if (obj != null) {
            JsonElement ele = obj.get(element);
            if (!ele.isJsonObject()) 
                return ele;
            else
                obj = ele.getAsJsonObject();
        } else {
            return null;
        }
    }
    return obj;
}
Pshemo
  • 122,468
  • 25
  • 185
  • 269
  • Alternatively path.split("[.]") can also be used as enclosing in [ ] depicts a literal character – prash Sep 21 '17 at 09:51
  • @prash Yes, which is already mentioned in the answer, along with few other ways :) – Pshemo Sep 21 '17 at 12:08
  • wow such a detailed one it is! .. Sorry i dint notice I just checked the code section .. ty for pointing out :-) – prash Sep 21 '17 at 12:12
  • I tried running it with : '{"menu":{"id":"file","value":"File","popup":{"menuitem":[{"value":"New","body":{"services":[{"id":"7d45d915-2e91-4d75-810f-c389339050cb"}]}}]}}}' tried to extract with "menu.popup.menuitem" and it failed – AviC Jan 12 '22 at 08:01
  • 1
    @AviC Worked for me. Calling `fromString(json, "menu.popup.menuitem")` returns array containing `[{"value":"New","body":{"services":[{"id":"7d45d915-2e91-4d75-810f-c389339050cb"}]}}]` which seems to match to what you wanted. – Pshemo Jan 12 '22 at 10:04
  • Pshemo ,thanks it did work , is it possible to preform modification on json using dotted path using same approach ?i.e remove – AviC Feb 08 '22 at 20:19