How does Polymorphism work with Gson (Retrofit) - java

Here is my Retrofit instance:
#Provides
#Singleton
ApiManager provideApiManager() {
RxJava2CallAdapterFactory rxAdapter = RxJava2CallAdapterFactory.create();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor())
.build();
Gson gson = new GsonBuilder().create();
GsonConverterFactory converterFactory = GsonConverterFactory.create(gson);
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(AppConstants.BASE_URL)
.addConverterFactory(converterFactory)
.addCallAdapterFactory(rxAdapter)
.client(okHttpClient)
.build();
return retrofit.create(ApiManager.class);
}
Model:
class AbstractMessage {
String id;
}
class TextMessage extends AbstractMessage {
String textMessage;
}
class ImageMessage extends AbstractMessage {
String url;
String text;
}
Request:
#GET("direct/messages")
Observable<List<AbstractMessage>> getMessages(#Header("Authorization") String authHeader, #Body RequestObject request);
Executing request:
apiManager.getMessages(authHeader, requestObject)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<AbstractMessage>>() {
#Override
public void accept(List<AbstractMessage> messages) throws Exception {
...
}
});
When I execute a request I receive a collection of AbstractMessage objects. The JSON can contain both text and image messages. In my case JSON converter creates AbstractMessage and maps only the id field. How can I make converter to create TextMessage and ImageMessage objects map all matching fields and then cast it to AbstractMessage. Or there may be some other solution.

You must create a RuntimeTypeAdapterFactory for the objects AbstractMessage, TextMessage and ImageMessage and then you must set it into the gson instance.
Suppose that you have those objects:
public class Animal {
protected String name;
protected String type;
public Animal(String name, String type) {
this.name = name;
this.type = type;
}
}
public class Dog extends Animal {
private boolean playsCatch;
public Dog(String name, boolean playsCatch) {
super(name, "dog");
this.playsCatch = playsCatch;
}
}
public class Cat extends Animal {
private boolean chasesLaser;
public Cat(String name, boolean chasesLaser) {
super(name, "cat");
this.chasesLaser = chasesLaser;
}
}
This below is the RuntimeTypeAdapter that you need in order to deserialize (and serialize) correctly those objects:
RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(runtimeTypeAdapterFactory)
.create();
The class RuntimeTypeAdapterFactory.java is not shipped with the Gson package, so you have to download it manually.
You can read more about the runtime adapter here and here
Please note that the title of your question should be "Polymorphism with Gson"
I hope it helps.

Related

Serialization error using Gson

I am using the following code for serialization:
public class JsonTransformer implements ResponseTransformer {
private Gson gson = new Gson();
#Override
public String render(Object model) {
GsonBuilder gsonBuilder = new GsonBuilder();
return gson.toJson(model);
}
}
I get error:
java.lang.UnsupportedOperationException: Attempted to serialize java.lang.Class: com.soul.seeker.models.Post. Forgot to register a type adapter?
How do I serialize a list a return it in the below code?
get("/data_on_page_load", "application/json", (Request request, Response response) -> {
List<Post> list = Post.findAll();
return list;
}, new JsonTransformer());
pojo class:
public class Post extends Model{
private String title;
private String details;
private String username;
private String userImage;
private String url;
private List categories;
//Getters and Setters removed for brevity
}
update:
When I tried to use it the below way:
public class JsonTransformer implements ResponseTransformer {
private Gson gson = new Gson();
#Override
public String render(Object model) {
GsonBuilder gsonBuilder = new GsonBuilder();
gson = gsonBuilder.registerTypeAdapterFactory(new ClassTypeAdapterFactory()).create();
return gson.toJson(model);
}
}
public class ClassTypeAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if(!Class.class.isAssignableFrom(typeToken.getRawType())) {
return null;
}
return (TypeAdapter<T>) new ClassTypeAdapter();
}
}
it returns unnecessary information in json object when I pass it back to client side:
Array[2]
0:Object
attributes:Object
cachedChildren:Object
cachedParents:Object
compositeKeyPersisted:false
dirtyAttributeNames:Array[0]
errors:Object
frozen:false
manageTime:true
metaModelLocal:Object
modelRegistryLocal:Object
__proto__:
Object1:Object
length:2
__proto__:Array[0]
It makes sense that ClassTypeAdapter is where I have modify the code to return proper list object as json and exclude irrelevant information, but I am still new to serialization.

toJson() of GSON library returning wrong json

I am using GSON library to pass json to server as header.
But it is not generating my expected json.
My Pojo class "TestRequest.java" is like:
public class TestRequest {
private String mobileNumber;
public TestRequest(String mobileNumber) {
this.mobileNumber = mobileNumber;
}
}
Here is my code to call the GSON class to make json:
Gson gson = new Gson();
TestRequest tt = new TestRequest("+8801913000000");
String json = gson.toJson(tt);
My expected json is :
{"mobileNumber":"+8801913000000"}
But I am getting:
{"aIf":"+8801913000000"}
Note: This code was working perfectly 2 days before.
Try to change your pojo class like
public class TestRequest implements Serializable {
#SerializedName("mobileNumber")
private String mobileNumber;
public TestRequest(String mobileNumber) {
this.mobileNumber = mobileNumber;
}
public String getMobileNumber() {
return mobileNumber;
}
public void setMobileNumber(String mobileNumber) {
this.mobileNumber = mobileNumber;
}
}
Let me know if not work

Gson + AutoValue error while trying to implement own deserializer

I have a Retrofit interface that I combine with RxJava. All my retrofit calls return Observable. Those "SomePojo" classes, I generate them online using Schema2Pojo sites.
I have a problem when making the following api call:
https://developers.themoviedb.org/3/search/2Y9y2LReFZdHFHbFA
As you can see, it is an array with two different types of objects, that I called "Media" and "Credit". These two classes I generated using Google's autovalue as follows:
#AutoValue
public abstract class Media implements Parcelable {
#SerializedName(value = "title", alternate = {"name"})
public abstract String title();
#Nullable
#SerializedName("vote_average")
public abstract String voteAverage();
#Nullable
#SerializedName("backdrop_path")
public abstract String backdropPath();
#Nullable
public abstract String adult();
public abstract String id();
#Nullable
public abstract String overview();
#Nullable
#SerializedName("original_language")
public abstract String originalLanguage();
#Nullable
#SerializedName("genre_ids")
public abstract List<String> genreIds();
#Nullable
#SerializedName(value = "release_date", alternate = {"first_air_date"})
public abstract String releaseDate();
#Nullable
#SerializedName(value = "original_title", alternate = {"original_name"})
public abstract String originalTitle();
#Nullable
#SerializedName("vote_count")
public abstract String voteCount();
#Nullable
#SerializedName("poster_path")
public abstract String posterPath();
#Nullable
public abstract String video();
#Nullable
#SerializedName("media_type")
public abstract String mediaType();
#Nullable
public abstract String popularity();
#Nullable
#SerializedName("origin_country")
public abstract List<String> originalCountry();
public static Media create(String title, String voteAverage, String backdropPath,
String adult, String id, String overview, String originalLanguage,
List<String> genreIds, String releaseDate, String originalTitle,
String voteCount, String posterPath, String video, String mediaType,
String popularity, List<String> originalCountry) {
return new AutoValue_Media(title, voteAverage, backdropPath, adult, id, overview,
originalLanguage, genreIds, releaseDate, originalTitle, voteCount, posterPath,
video, mediaType, popularity, originalCountry);
}
public static TypeAdapter<Media> typeAdapter(Gson gson) {
return new AutoValue_Media.GsonTypeAdapter(gson);
}
}
And:
#AutoValue
public abstract class Credit implements Parcelable {
public abstract String id();
#SerializedName("credit_id")
public abstract String creditId();
#Nullable
public abstract String department();
public abstract String name();
#Nullable
#SerializedName(value = "job", alternate = {"character"})
public abstract String job();
#Nullable
#SerializedName("profile_path")
public abstract String profilePath();
#Nullable
public abstract String order();
#Nullable
#SerializedName("cast_id")
public abstract String castId();
public static Credit create(String id, String creditId, String department, String name, String job,
String profilePath, String order, String castId) {
return new AutoValue_Credit(id, creditId, department, name, job, profilePath, order, castId);
}
public static TypeAdapter<Credit> typeAdapter(Gson gson) {
return new AutoValue_Credit.GsonTypeAdapter(gson);
}
}
To resolve the problem created by the array with two different kind of objects, I made the POJO return by this call implement its own JsonDeserializer:
public class MediaListPojo {
#SerializedName("results")
private List<Media> movies;
private List<Credit> credits;
private Dates dates;
private String page;
private String total_pages;
private String total_results;
public List<Media> getMedia() {
return movies;
}
public void setMovies(List<Media> movies) {
this.movies = movies;
}
public List<Credit> getCredits() {return credits;}
public void setCredits(List<Credit> credits) {this.credits = credits;}
public Dates getDates() {
return dates;
}
public void setDates(Dates dates) {
this.dates = dates;
}
public String getPage() {
return page;
}
public void setPage(String page) {
this.page = page;
}
public String getTotal_pages() {
return total_pages;
}
public void setTotal_pages(String total_pages) {
this.total_pages = total_pages;
}
public String getTotal_results() {
return total_results;
}
public void setTotal_results(String total_results) {
this.total_results = total_results;
}
#Override
public String toString() {
return "MediaListPojo{" +
"movies=" + movies +
", credits=" + credits +
", dates=" + dates +
", page='" + page + '\'' +
", total_pages='" + total_pages + '\'' +
", total_results='" + total_results + '\'' +
'}';
}
public static class MediaListPojoDeserializer implements JsonDeserializer<MediaListPojo> {
#Override
public MediaListPojo deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
MediaListPojo mediaListPojo = new Gson().fromJson(json, MediaListPojo.class);
JsonObject jsonObject = json.getAsJsonObject();
if (jsonObject.has("results")) {
JsonArray jsonArray = jsonObject.getAsJsonArray("results");
List<Credit> credits = new ArrayList<>();
Credit credit;
for (JsonElement element : jsonArray) {
JsonObject current = element.getAsJsonObject();
if (current.get("media_type").getAsString().equals("person")) {
credit = new Gson().fromJson(current, Credit.class);
credits.add(credit);
}
}
mediaListPojo.setCredits(credits);
}
return mediaListPojo;
}
}
}
The main idea behind this json deserializer is: "Use the default type adapter for this class and then set the Credit objects using this JsonDeserializer"
However, for some reason, I get the following error while deserializing:
java.lang.RuntimeException: Failed to invoke public Media() with no args
...
Caused by: java.lang.InstantiationException: Can't instantiate abstract class Media
at java.lang.reflect.Constructor.newInstance(Native Method)
It shouldn't try to instantiate the abstract superclass, but use AutoValue's generated Type Adapter.
This is how I built my retrofit instance:
class Creator {
public static MovieService newMovieService() {
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new AutoValueGson_MyAdapterFactory())
.registerTypeAdapter(MediaListPojo.class, new MediaListPojo.MediaListPojoDeserializer())
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.create();
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(NetworkUtil.makeQueryInterceptor("api_key", BuildConfig.MY_API_KEY))
.build();
Retrofit retrofit = new Retrofit.Builder()
.client(client)
.baseUrl(MovieService.ENDPOINT)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
return retrofit.create(MovieService.class);
Can you help me understand what I did wrong?
Well, I found the solution 5 minutes after posting the question, but since I think other people might struggle with this as well. I'll share the solution:
Basically, inside of my JsonDeserializer, I was using a new instance of a Gson object when, in fact, this is a mistake.
The typeadapterfactory that I registered while creating my retrofit instance is where all of the other TypeAdapters live.
Therefore, calling
Gson gson = new Gson();
Doesn't supply the type adapters that I needed to deserialize the rest of the object.
I hope it helps.
you need to register your TypeAdapterFactory like so when creating your instance of Gson. for me I did this in my dagger 2 module like below.
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(GsonAdapterFactory.create())
.create();
hope this helps.

Gson property order in android

I have integrated Gson to create the json used in a request for an android application.
Here is my model class
public class TwitterUser {
#Expose
public String gid;
public String icon_url;
public Boolean is_app_user;
#Expose
public String displayName;
public TwitterUser(String l, String i, String url, Boolean app_user) {
gid = i;
displayName = l;
icon_url = url;
is_app_user = app_user;
}
public TwitterUser(String l, String i) {
gid = i;
displayName = l;
}
public String getGid() {
return gid;
}
public void setGid(String gid) {
this.gid = gid;
}
public String getIcon_url() {
return icon_url;
}
public void setIcon_url(String icon_url) {
this.icon_url = icon_url;
}
public Boolean getIs_app_user() {
return is_app_user;
}
public void setIs_app_user(Boolean is_app_user) {
this.is_app_user = is_app_user;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
Here is how i create the json request
Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
gson.toJson(twitterUser));
But when I send the request to the server - the order will be rejected. I have to change the request's field order to stay:
gid
displayName
but gson creates other way around, is there any way to achieve this.
Gson doesn't support definition of property order out of the box, but there are other libraries that do. Jackson allows defining this with #JsonPropertyOrder, for example.
But of course Gson has it's way so you can do it by creating your very own Json serializer:
public class TwitterUserSerializer implements JsonSerializer<TwitterUser> {
#Override
public JsonElement serialize(TwitterUser twitterUser, Type type, JsonSerializationContext context) {
JsonObject object = new JsonObject();
object.add("gid", context.serialize(twitterUser.getGid());
object.add("displayName", context.serialize(twitterUser.getDisplayName());
// ...
return object;
}
}
Then of course you need to pass this serializer to Gson during Setup like this:
Gson gson = new GsonBuilder().registerTypeAdapter(TwitterUser.class, new TwitterUserSerializer()).excludeFieldsWithoutExposeAnnotation().create();
String json = gson.toJson(twitterUser);
See also:
Gson User Guide - Custom serializers and deserializers

How to expose a method using GSon?

Using Play Framework, I serialize my models via GSON. I specify which fields are exposed and which aren't.
This works great but I'd also like to #expose method too. Of course, this is too simple.
How can I do it ?
Thanks for your help !
public class Account extends Model {
#Expose
public String username;
#Expose
public String email;
public String password;
#Expose // Of course, this don't work
public String getEncodedPassword() {
// ...
}
}
The best solution I came with this problem was to make a dedicated serializer :
public class AccountSerializer implements JsonSerializer<Account> {
#Override
public JsonElement serialize(Account account, Type type, JsonSerializationContext context) {
JsonObject root = new JsonObject();
root.addProperty("id", account.id);
root.addProperty("email", account.email);
root.addProperty("encodedPassword", account.getEncodedPassword());
return root;
}
}
And to use it like this in my view:
GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(Account.class, new AccountSerializer());
Gson parser = gson.create();
renderJSON(parser.toJson(json));
But having #Expose working for a method would be great: it would avoid making a serializer just for showing methods!
Check out Gson on Fire: https://github.com/julman99/gson-fire
It's a library I made that extends Gson to handle cases like exposing method, results Post-serialization, Post-deserialization and many other things that I've needed over time with Gson.
This library is used in production in our company Contactive (http://goo.gl/yueXZ3), on both Android and the Java Backend
Gson's #Expose seem to only be supported on fields. There is an issue registered on this: #Expose should be used with methods.
Couple different options based on Cyril's answer:
Custom serializer with a shortcut:
public static class Sample
{
String firstName = "John";
String lastName = "Doe";
public String getFullName()
{
return firstName + " " + lastName;
}
}
public static class SampleSerializer implements JsonSerializer<Sample>
{
public JsonElement serialize(Sample src, Type typeOfSrc, JsonSerializationContext context)
{
JsonObject tree = (JsonObject)new Gson().toJsonTree(src);
tree.addProperty("fullName", src.getFullName());
return tree;
}
}
public static void main(String[] args) throws Exception
{
GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(Sample.class, new SampleSerializer());
Gson parser = gson.create();
System.out.println(parser.toJson(new Sample()));
}
-OR-
Annotation based serializer
public static class Sample
{
String firstName = "John";
String lastName = "Doe";
#ExposeMethod
public String getFullName()
{
return firstName + " " + lastName;
}
}
public static class MethodSerializer implements JsonSerializer<Object>
{
public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context)
{
Gson gson = new Gson();
JsonObject tree = (JsonObject)gson.toJsonTree(src);
try
{
PropertyDescriptor[] properties = Introspector.getBeanInfo(src.getClass()).getPropertyDescriptors();
for (PropertyDescriptor property : properties)
{
if (property.getReadMethod().getAnnotation(ExposeMethod.class) != null)
{
Object result = property.getReadMethod().invoke(src, (Object[])null);
tree.add(property.getName(), gson.toJsonTree(result));
}
}
}
catch (Exception ex)
{
ex.printStackTrace();
}
return tree;
}
}
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD) //can use in method only.
public static #interface ExposeMethod {}
public static void main(String[] args) throws Exception
{
GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(Sample.class, new MethodSerializer());
Gson parser = gson.create();
System.out.println(parser.toJson(new Sample()));
}

Categories