7

I am trying to deserialize a JSON array using GSON. All of my nested objects are embedded inside an "embedded" object.

{
    "Book": {
        "name": "Book 1",
        "published": 1999,
        "links": {
          "url": "www.book1.com"
        },
        "embedded": {
            "Author": {
                "name": "John Doe",
                "links": {
                    "url": "www.johndoe.com"
                }
            }
        }
    }
}

I could also have a situation like this:

{
    "Book": {
        "name": "Book 1",
        "published": 1999,
        "links": {
          "url": "www.book1.com"
        },
        "embedded": {
            "Publisher": {
                "name": "Publishing Company",
                "links": {
                    "url": "www.publishingcompany.com"
                }
            }
        }
    }
}

This is an extremely simple example. Some of my objects may be nested 2 or 3 levels deep, and all are in an "embedded" object. Also, each object has a nested "url" inside a "links" object. I have around 20 different model objects, each with several fields, and everyone of them have the "embedded" object. I started to write custom deserializers for each model, but that seems to miss the whole point of using gson, and I may not always know what the embedded object is.

I found this answer, but it was for serializing objects. I have been trying to figure this out for a while now and have not found anything that works.

My Book model looks like this:

public class Book {
    String name;
    int published;
    String url;
    Author author;
    Publisher publisher;
}

Author class:

public class Author {
    String name;
    String url;
}

Publisher class:

public class Publisher {
    String name;
    String url;
}

And here is my Book deserializer so far:

public class BookDeserializer implements JsonDeserializer<Book> {
    @Override
    public Book deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

        final JsonObject jsonObject = json.getAsJsonObject();

        Book book = new Book();
        book.setName(jsonObject.get("name").getAsString());
        book.setPublished(jsonObject.get("published").getAsInt());
        String url = jsonObject.getAsJsonObject("links").get("url").getAsString();
        book.setUrl(url);

        // 1) How to get rid of this and skip to the "real" nested object?
        final JsonObject embeddedObject = jsonObject.getAsJsonObject("embedded");

        // 2) See what the "embedded" object actually is.
        String embeddedModel;
        Set<Map.Entry<String, JsonElement>> entrySet = embeddedObject.entrySet();
        for (Map.Entry<String, JsonElement> entry : entrySet) {

            // Author or Publisher
            embeddedModel = entry.getKey();
        }

        // We have the model's key, now add code here to deserialize whatever the object is

        return book;
    }
}

I still have to parse the json and set each field for Book. Then, I would have to add code to determine and use the correct deserializer for the nested object. Looks like I still would need a custom deserializer for each object to get the "url". I am fairly new to gson, so maybe there is just something that I am overlooking, but it seems that I might as well just manually parse all of the json and not even use gson. Maybe there is a way to flatten out json?

Any ideas on how to parse this and still use the convenience of gson, or is this even possible? Maybe Jackson could handle this better?

Community
  • 1
  • 1
Mark
  • 1,130
  • 3
  • 17
  • 32

3 Answers3

1

Create a class called embedded and add it as a field in Book:

public class Book {
    String name;
    int published;
    Embedded embedded;
}

Then create an embedded class:

public class Embedded {
    Author Author;
    Publisher Publisher;
}

Just model your classes after your JSON

GreyBeardedGeek
  • 29,460
  • 2
  • 47
  • 67
user489041
  • 27,916
  • 55
  • 135
  • 204
  • This is the way I initially thought to do this, but hoping there was a more "elegant" solution. It seems like a waste to create an Links and Embedded classes (there will be many more like this), each with objects that I may not even use. I guess it is easier than creating custom deserializers for each though. – Mark Jan 28 '15 at 16:49
  • we use [Dozer](http://dozer.sourceforge.net/) in our company to map between this classes. actually we make a flat object of response we want to send and use dozer to map them. it help for automatic mapping the beans to each other – alizelzele Jan 29 '15 at 06:52
1

My first thought was to parse the JSON and hack it around but it looks like GSON JsonObjects are immutable.

I would therefore write a simple stream parser that looks for "embedded": { and "links": { and remove them. Run a simple bracket counter too to remove the matching close bracket. If time permits I might throw one together.

BTW - Your sample JSON is missing a comma - paste it here to check it.

Added:- The stream parser got out of hand - although it would have been the tidier option. If you can find a JSON stream parser like SAX does for XML you may be able to do it better that way.

Second mechanism assumes you can fit the whole of your JSON in a String in memory. Not ideal but probably an acceptable solution for most setups. This then uses a simple regex plus a bracket counter to remove the required parts.

/**
 * Finds the first matching close brace - assuming an open brace has just been removed from the `start` position.
 */
private int closeBrace(StringBuilder s, int start) {
    int count = 1;
    boolean inQuotes = false;
    for (int i = start; i < s.length(); i++) {
        char ch = s.charAt(i);
        // Special case escapes.
        if (ch != '\\') {
            switch (ch) {
                case '"':
                    inQuotes = !inQuotes;
                    break;
                case '{':
                    if (!inQuotes) {
                        count += 1;
                    }
                    break;
                case '}':
                    if (!inQuotes) {
                        count -= 1;
                        if (count == 0) {
                            return i;
                        }
                    }
                    break;
            }
        } else {
            // Escape character - skip the next character.
            if (i < s.length()) {
                i += 1;
            }
        }
    }
    // Failed to find
    return s.length();
}

/**
 * Removes the JSON specified.
 */
private String hack(String json, String remove) {
    // Transfer to an sb for slicing and dicing.
    StringBuilder s = new StringBuilder(json);
    // Build my pattern
    Pattern p = Pattern.compile("\"" + remove + "\"\\s*:\\s*\\{");
    // Make my Matchjer.
    Matcher m = p.matcher(s);
    // Is it there?
    while (m.find()) {
        int start = m.start();
        int end = m.end();
        // Kill the match.
        s.delete(start, end);
        // Walk forward to find the close brace.
        end = closeBrace(s, start);
        // And remove it.
        if (end < s.length()) {
            s.delete(end, end + 1);
        }
        // Rebuild the matcher.
        m = p.matcher(s);
    }
    return s.toString();
}

private void test(String json) {
    JsonParser parser = new JsonParser();
    JsonElement e = parser.parse(json);
    System.out.println(e);
}

public void test() {
    String json = "{'Book': {'name': 'Book \\'1\\'','published': 1999,'links': {'url': 'www.book1.com'},'embedded': {'Publisher': {'name': 'Publishing Company','links': {'url': 'www.publishingcompany.com'}}}}}".replace("'", "\"");
    test(json);
    json = hack(json, "embedded");
    test(json);
    json = hack(json, "links");
    test(json);
}

prints:

{"Book":{"name":"Book \"1\"","published":1999,"links":{"url":"www.book1.com"},"embedded":{"Publisher":{"name":"Publishing Company","links":{"url":"www.publishingcompany.com"}}}}}
{"Book":{"name":"Book \"1\"","published":1999,"links":{"url":"www.book1.com"},"Publisher":{"name":"Publishing Company","links":{"url":"www.publishingcompany.com"}}}}
{"Book":{"name":"Book \"1\"","published":1999,"url":"www.book1.com","Publisher":{"name":"Publishing Company","url":"www.publishingcompany.com"}}}

which looks a bit like what you are looking for.

OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213
  • Thanks for the help and I have corrected the JSON. This sounds like a good idea and some sample code would be great! Gson does support streaming, is that what you are referring to? – Mark Feb 03 '15 at 05:23
  • @Mark - See **Added** - Not a very pretty solution but it should work for small to medium JSON objects. – OldCurmudgeon Feb 03 '15 at 09:46
  • Thank you for providing the code example. However, I'm going to go with just creating the extra classes so my java models match the json. Looks like gson might not provide a built-in way to unwrap selected nested objects. – Mark Feb 04 '15 at 05:03
0

I think that you are looking for sth like that: flatten

That tool can help you to omit some embedded classes. You will have less classes and cleaner code. In your Book class use this:

@Flatten("embedded::Author")
private Author author;

This way you can collapse one level. The same way you can move links to author. In your Author class:

@Flatten("links::url")
private String url;

If you want to go deeper you can move 2 levels higher in the same way. For example:

@Flatten("embedded::Author::name")
private String authorName;

Here you will have author's name inside Book class.

Hope it helps.

konned
  • 53
  • 6
  • 1
    Please don't just post some tool or library as an answer. At least demonstrate [how it solves the problem](http://meta.stackoverflow.com/a/251605) in the answer itself. – Dwhitz Jul 22 '19 at 12:50
  • @Dwhitz thanks for comment :) I edited the answer. Too busy today :/ – konned Jul 22 '19 at 13:56