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://.....