3

I have a json object that looks like this:

{
    "user": {
        "id": 1234
        ... 
        "photos": [
            {
                "url": "http://....."
                ...
            },
            {
                "url": "http://....."
                ...
            }
        ]
    }
}

I want to write a custom deserializer for both user and photos.

so I have:

public class User {
    private long id;
    private ArrayList<Photo> photos;
    ... 

    public static class Deserializer implements JsonDeserializer<User> {
        ... // does the custom serialization of the User object 
    }  
}

public class Photo {
    private String url;
    ... 

    public static class Deserializer implements JsonDeserializer<Photos> {
        ... // does the custom serialization of the Photo object 
    }  
}

and when initializing I do this:

new GsonBuilder()
   .registerTypeAdapter(User.class, new User.Deserializer());
   .registerTypeAdapter(Photos.class, new Photos.Deserializer());

However, when i deserialize the User class, it hits the User's deserializer but never hits the Photo's deserializer. But if i get a json with the photo object not nested in the user json object like this:

{
    "photos": [
         {
             "url": "http://....."
              ...
         },
         {
             "url": "http://....."
              ...
         },
         {
             "url": "http://....."
              ...
         }
]

it will properly hit the Photo's deserializer

3
  • Could you please tell why you need custom deserializers for this case? Commented Mar 13, 2017 at 21:34
  • The example I gave above was a very simple one. the objects i use in my app is much more complicated. Commented Mar 13, 2017 at 21:58
  • Hi, I saw your question at GitHub: github.com/google/gson/issues/1034 -- could you please close it since you are aware of how Gson type adapters work now? Thanks. Commented Mar 15, 2017 at 7:10

2 Answers 2

5

In short, there is a non-formal rule: once you declare a type adapter (or a (de)serializer that share the same concept in principle) for a certain type, then you have to manage its instantiation and its child fields yourself. Thus, when you deserialize the top-most User, its id and photos are deserialized on your own. Note that the Photo.Deserializer is invoked once you request for it explicitly like gson.fromJson(..., Photo.class) or apply it implicitly (for the latter Gson uses built-in strategies by default, please see ReflectiveTypeAdapterFactory for example) via deserialization contexts. The same principle goes to User if you don't bind User.Deserializer, therefore Gson uses ReflectiveTypeAdapterFactory.Adapter<T> that just iterates through all fields itself using reflection. Even shorter: Gson does not merge multiple strategies (at least by default), so you either delegate object construction and setting up to Gson, or instantiate it completely.

Knowing that, User.Deserializer can be implemented as the following:

final class User {

    final long id;
    final List<Photo> photos;

    private User(final long id, final List<Photo> photos) {
        this.id = id;
        this.photos = photos;
    }

    static final class Deserializer
            implements JsonDeserializer<User> {

        private static final Type photoListType = new TypeToken<List<Photo>>() {
        }.getType();

        @Override
        public User deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context) {
            // Note that you must pick up properties first
            final JsonObject jsonObject = jsonElement.getAsJsonObject();
            return new User(
                    // And then delegate them to the deserialization context specifying the target type
                    context.deserialize(jsonObject.get("id"), long.class),
                    // You can deconstruct JsonElement recursively, but deserialization context respects Gson context built with GsonBuilder
                    // This also does trigger the Photo.Deserializer
                    context.deserialize(jsonObject.get("photos"), photoListType)
            );
        }

    }

}

I'm assuming Photos in your code is a typo and it's supposed to be Photo. If it's not, then a similar solution might be implemented for Photos.

final class Photo {

    final String url;

    private Photo(final String url) {
        this.url = url;
    }

    static final class Deserializer
            implements JsonDeserializer<Photo> {

        @Override
        public Photo deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context) {
            final JsonObject jsonObject = jsonElement.getAsJsonObject();
            return new Photo(
                    // jsonObject.get("url").getAsString() can be more simple, but it does not respect Gson instance configuration
                    context.deserialize(jsonObject.get("url"), String.class)
            );
        }

    }

}

How it can be used:

final class Wrapper {

    final User user;

    private Wrapper(final User user) {
        this.user = user;
    }

}
final Gson gson = new GsonBuilder()
        .registerTypeAdapter(User.class, new User.Deserializer())
        .registerTypeAdapter(Photo.class, new Photo.Deserializer())
        .create();
final Wrapper wrapper = gson.fromJson(JSON, Wrapper.class);
System.out.println(wrapper.user.id);
wrapper.user.photos.forEach(p -> System.out.println(p.url));

The output:

1234
http://.....
http://.....

Sign up to request clarification or add additional context in comments.

3 Comments

ah, that's what i was afraid of. and there is no way to merge multiple strategies (you mentioned not by default)
@Sree Yep, this is the cost of simplicity. An example custom type adapter factory than can be used to merge a few strategies together: github.com/google/gson/blob/master/extras/src/main/java/com/… -- a delegate-first-and-then-post-process-somehow approach.
In other words, you only need to declare a type adapter for the parent type - the children's type adapters will be redundant?
1

I think the classes don't match the json file. In this json file:

{
    "user": {
        "id": 1234
        ... 
        "photos": {
            "abc": {
                "url": "http://....."
                ...
            }
        }
    }
}

You have

public class User {
    private int id;
    private Photos photos;
} 

public class Photos {
   private MyUrl abc;
   private MyUrl bcd;
   private MyUrl cde;
   ...
}

public class MyUrl {
    private String url;
}

To get an ArrayList of the photos the json should look like this (note the square brackets and its content):

{
    "user": {
        "id": 1234
        ... 
        "photos": [
             { "url": "http://....." },
             { "url": "http://....." },
                ...
             { "url": "http://....." }
            ]
        }
    }
}

This last jason responds to:

public class User {
    private int id;
    ...
    private ArrayList<Photo> photos;
} 

public class Photo{
    private String url;
}

2 Comments

sorry, that was a mistake in the example. i updated it to adhere to my java classes. It is hitting the deserializer otherwise, so i think the code is working. it's just something about it being called from another deserializer that seems to be the issue
Where it says Photos it is really Photo. It is a typo. Not two different clases. Right?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.