Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android - Inheritance and abstract classes in GSon + RetroFit

I have the following class hierarchy

public abstract class SyncModel {
    @Expose
    @SerializedName("id")
    private Long globalId;

    @Expose
    protected DateTime lastModified;
    
    /* Constructor, methods... */
}

public class Event extends SyncModel {
    @Expose
    private String title;
    
    /* Other fields, constructor, methods... */
}

I need to send an Event instance to backend.

Case 1. @Body

When I post the Event instance in a request body, it is serialized fine.

RetroFit Java interface:

public interface EventAPI {
    @POST("/event/create")
    void sendEvent(@Body Event event, Callback<Long> cbEventId);
}

RetroFit log:

D   Retrofit    ---> HTTP POST http://hostname:8080/event/create
D   Retrofit    Content-Type: application/json; charset=UTF-8
D   Retrofit    Content-Length: 297
D   Retrofit    {"title":"Test Event 01",...,"id":null,"lastModified":"2015-07-09T14:17:08.860+03:00"}
D   Retrofit    ---> END HTTP (297-byte body)

Case 2. @Field

But when I post the Event instance in a request parameter, only abstract class is serialized.

RetroFit Java interface:

@FormUrlEncoded
@POST("/event/create")
void sendEvent(@Field("event") Event event, Callback<Long> cbEventId);

RetroFit log:

D   Retrofit    ---> HTTP POST http://hostname:8080/event/create
D   Retrofit    Content-Type: application/x-www-form-urlencoded; charset=UTF-8
D   Retrofit    Content-Length: 101
D   Retrofit    event=SyncModel%28globalId%3Dnull%2C+lastModified%3D2015-07-09T13%3A36%3A33.510%2B03%3A00%29
D   Retrofit    ---> END HTTP (101-byte body)

Notice the difference.

Questions

Why?
How can I send a serialized Event instance to backend in a request parameter?
Do I need to write a custom JSON serializer for abstract class? (example: Polymorphism with JSON)
Or is it a RetroFit specific feature (to ignore child classes)?

I've also noticed that in the 2nd case globalId field serialized name is globalId, but it should be id! It makes me think that RetroFit uses a different GsonConverter for @Field than for @Body parameters...


Configuration

Gradle dependencies

compile 'com.squareup.retrofit:retrofit:1.9.+'
compile 'com.squareup.okhttp:okhttp:2.3.+'
compile 'net.danlew:android.joda:2.8.+'
compile ('com.fatboyindustrial.gson-jodatime-serialisers:gson-jodatime-serialisers:1.1.0') {  // GSON + Joda DateTime
    exclude group: 'joda-time', module: 'joda-time'
}

REST client

public final class RESTClient {

    // Not a real server URL
    public static final String SERVER_URL = "http://hostname:8080";

    // one-time initialization
    private static GsonBuilder builder = new GsonBuilder()
            .serializeNulls()
            .excludeFieldsWithoutExposeAnnotation()
            .setDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'");
    // Joda DateTime type support
    private static Gson gson = Converters.registerDateTime(builder).create();

    private static RestAdapter restAdapter = new RestAdapter.Builder()
            .setLogLevel(RestAdapter.LogLevel.FULL)     // for development
            .setEndpoint(SERVER_URL)
            .setConverter(new GsonConverter(gson))    // custom converter
            .build();

    private static final EventAPI eventService = restAdapter.create(EventAPI.class);
    /* + Getter for eventService */

    static {
        // forget them
        restAdapter = null;
        gson = null;
        builder = null;
    }

}

Call

RESTClient.getEventService().sendEvent(event, new Callback<Long>() {/* ... */});
like image 766
naXa Avatar asked Jul 09 '15 12:07

naXa


1 Answers

Take a look at @Field's documentation. It says:

Values are converted to strings using String#valueOf(Object) and then form URL encoded.

String#valueOf(Object) makes a call to Object#toString() inside. I suppose your SyncModel has a toString() method and Event does not. When Retrofit calls String.valueOf(event), SyncModel#toString() is called instead of Event#toString(). That's why you don't see title in the Retrofit logs.

Gson doesn't play any role at all when converting @Field parameters. It can be though - you can make your toString() method look something like this:

@Override
public String toString() {
    return GsonProvider.getInstance().toJson(this);
}

Put this inside your abstract SyncModel class and it should work for Event as well.

like image 182
Egor Neliuba Avatar answered Oct 21 '22 07:10

Egor Neliuba