3

I'm migrating to Retrofit from Asynctask + Loader for our core networking. It was going well till I encountered ClassCastExceptions where they previously didn't occur:

ImagePojo imagePojo = (ImagePojo) mediaPojo; // Error: cannot cast MediaPojo to ImagePojo

Here's the skeleton of my POJOs in the hierarchy:

Parent/Generic media object

public class MediaPojo extends CustomObservable implements Parcelable {
    @SerializedName("media_id")
    private String mMid;
    @SerializedName("type")
    private String mType;
    @SerializedName("title")
    public MediaPojo() {
    }

    @Override
    public int describeContents() {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mNid);
        dest.writeString(mType);
        dest.writeString(mTitle);
    }

    public static final Parcelable.Creator<MediaPojo> CREATOR = new Parcelable.Creator<MediaPojo>() {
        @Override
        public MediaPojo createFromParcel(Parcel source) {
            return new MediaPojo(source);
        }

        @Override
        public MediaPojo[] newArray(int size) {
            return new MediaPojo[size];
        }
    };
...
} 

// defines fields common to all media types

Video media type

public class VideoPojo extends MediaPojo implements Parcelable {
    @SerializedName("video_id")
    private String mVid;
    @SerializedName("video_url")
    private String mVideoUrl;
    @SerializedName("thumbnail")
    private String mThumbnail ...
}

Image media type POJO

public class ImagePojo extends MediaPojo implements Parcelable {
    @SerializedName("image_url")
    private String mImageUrl;
    @SerializedName("width")
    private String mImageWidth;
    @SerializedName("height")
    private String mImageHeight
    ...
}

// Other types: TextPojo, InfoGraphicPojo ...

So far, the only change I made to the POJOs in the migration was to add the @SerializedName annotations to allow Retrofit to automatically map the JSON fields to defined POJO instance variables. I find it necessary to do the generic MediaPojo to specific type POJO conversions at runtime as my REST calls to api.mydomain.com/media can return JSON representing media of various types (video, image, etc).

Is there any way to make Retrofit work around my already existing object inheritance structure? Or could it the way I do serialization with the Parcelable interface?

kip2
  • 6,473
  • 4
  • 55
  • 72

1 Answers1

2

Yes, the issue is the gson always creates the type in the retrofit call. You need to provide a custom deserializer to figure out the object type based on your type field. Here is a sketch of what that might look like.

public class ModiaPojoDeserializer implements JsonDeserializer<Model> {

    private static final String TYPE_FIELD = "type";
    private static final String IMAGE_TYPE = "image";
    private static final String VIDEO_TYPE = "video";

    @Override
    public Model deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        if (json.isJsonObject() && json.getAsJsonObject().has(TYPE_FIELD)) {
            JsonObject jsonObject = json.getAsJsonObject();
            final String type = jsonObject.get(TYPE_FIELD).getAsString();
            if (VIDEO_TYPE.equals(type)) {
                return context.deserialize(json, VideoPojo.class);
            } else if (IMAGE_TYPE.equals(type)) {
                return context.deserialize(json, ImagePojo.class);
            } // ...
            // If you need to deserialize as MediaPojo,
            // deserialize without the current context 
            // or you will infinite loop
            return new Gson().fromJson(json, typeOfT);

        } else {
            // Return a blank object on error, could use null
            return new MediaPojo();
        }
    }
}

Then you would create a custom gson object using Gson.Builder using your custom deserializer -

 Gson gson = new GsonBuilder()
     .registerTypeAdapter(MediaPojo.class, new MediaPojoDeserializer())
     .create();

and then add it when creating your RestAdapter --

RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("https://api.mydomain.com")
    .setConverter(new GsonConverter(gson))
    .build();
iagreen
  • 31,470
  • 8
  • 76
  • 90
  • Brilliant. I suppose symmetric serialization is not required? As in, we don't need to create a custom serializer if we create a custom deserializer? – kip2 Aug 28 '15 at 12:16
  • 2
    Correct, it is not required. It will use the default serialization if you don't specify one and only specify a custom deserailizer. The default serialization should work fine for your case. – iagreen Aug 28 '15 at 13:31