0

I am using RetroFit 1.9 with a GSON converter that has been working well for me so far. Now I am trying to marshall the List of custom Parcelable objects received in the Callback, and I am met with a ClassCastException:

02-02 09:53:49.921 13030-13030/com.example.app E/AndroidRuntime: FATAL EXCEPTION: main
            Process: com.example.app, PID: 13030
            java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to android.os.Parcelable
                at android.os.Parcel.writeTypedList(Parcel.java:1166)
                at com.example.common.util.ParcelableUtil.marshall(ParcelableUtil.java:37)
                at com.example.app.service.WearableMessageService$1.success(WearableMessageService.java:130)
                at com.example.app.service.WearableMessageService$1.success(WearableMessageService.java:120)
                at retrofit.CallbackRunnable$1.run(CallbackRunnable.java:45)
                at android.os.Handler.handleCallback(Handler.java:739)
                at android.os.Handler.dispatchMessage(Handler.java:95)
                at android.os.Looper.loop(Looper.java:148)
                at android.app.ActivityThread.main(ActivityThread.java:5417)
                at java.lang.reflect.Method.invoke(Native Method)
                at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
                at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

This is my callback method:

new Callback<List<MyObject>>() {
    @Override
    public void success(List<MyObject> objects, Response response) {
        mByteArray = ParcelableUtil.marshall(objects);
    }

    @Override
    public void success(RetrofitError error) {
        Timber.w("Failed to retrieve events, with message " + error.getMessage());
    }
}

So I added a for loop in the success method, just before I call ParcelableUtil.marshall(), to test why I am getting a list of LinkedTreeMap objects instead of my objects:

for(MyObject object : objects) {
    Timber.d(object.getTitle());
}

Not only does this print the correct title of each object, miraculously the rest of the code works! ParcelableUtil no longer throws an error, and I receive a byte array that I am later able to unmarshall perfectly.

How does a list of LinkedTreeMap objects change to a list of my objects after it is observed in a loop? Why am I getting a list of LinkedTreeMap objects in the first place? What is going on here?


MyObject class:

public class MyObject implements Parcelable {

    @SerializedName("title") private String mTitle;
    @SerializedName("location") private String mLocation;

    @SerializedName("start_date") private Date mStartDate;
    @SerializedName("end_date") private Date mEndDate;

    public MyObject() {
        /* Required empty constructor */
    }

    public Event(Parcel in) {
        mTitle = in.readString();
        mLocation = in.readString();

        mStartDate = new Date(in.readLong());
        mEndDate = new Date(in.readLong());
    }

    public String getTitle() {
        return mTitle;
    }

    public String getLocation() {
        return mLocation;
    }

    public void getStartDate() {
        return mStartDate();
    }

    public void getEndDate() {
        return mEndDate();
    }

    @Override
    public in describeContents() {
        return 0;
    }

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

        dest.writeLong(mStartDate.getTime());
        dest.writeLong(mEndDate.getTime());
    }

    public static final Creator<MyObject> CREATOR = new Creator<MyObject>() {
        @Override
        public MyObject createFromParcel(Parcel in) {
            return new MyObject(in);
        }

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

A sample JSON feed for MyObject:

{
    "items": [
        {
            "title":"Object 1"
            "location":"Location 1"
            "start_date":"2016-02-02 15:30:00"
            "end_date":"2016-02-02 19:00:00"
        }
        {
            "title":"Object 2"
            "location":"Location 2"
            "start_date":"2016-02-02 18:00:00"
            "end_date":"2016-02-03 18:00:00"
        }
    ]
}

The GSON TypeAdapterFactory in use:

public class DataAdapterFactory implements TypeAdapterFactory {

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {

        final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
        final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);

        return new TypeAdapter<T>() {

            @Override
            public void write(JsonWriter out, T value) throws IOException {
                delegate.write(out, value);
            }

            @Override
            public void read(JsonReader in) throws IOException {
                JsonElement jsonElement = elementAdapter.read(in);

                if(jsonElement.isJsonObject()) {
                    JsonObject jsonObject = jsonElement.getAsJsonObject();

                    if(jsonObject.has("items") && jsonObject.get("items".isJsonArray()) {
                        jsonElement = jsonObject.getAsJsonArray("items");
                    }

                    return delegate.fromJsonTree(jsonElement);
                }
            }
        }.nullSafe();
    }
}
Craig Gidney
  • 17,763
  • 5
  • 68
  • 136
Bryan
  • 14,756
  • 10
  • 70
  • 125
  • 1
    Interesting - what does your JSON look like? And how is `MyObject` defined? – david.mihola Feb 02 '16 at 18:12
  • @david.mihola Added after the break. It's just a simple POJO that uses the `@SerializedName` annotation, and a standard implementation of `Parcelable`. – Bryan Feb 02 '16 at 19:22
  • 1
    Thanks! Doesn't look too weird... And the declaration in your Retrofit interface? I am just a bit confused because your `Callback` has the type parameter `List`. But the JSON looks like some object that *contains* such a `List` as a value with key `items`. How did you manage to do that? – david.mihola Feb 02 '16 at 19:30
  • 1
    Oh, I see - with a custom `TypeAdapterFactory`. Just to see if the problem lies there - could you try just creating a wrapper class that directly matches the JSON and contains the list? – david.mihola Feb 02 '16 at 19:33
  • @david.mihola Ah, my GSON `TypeAdapterFactory` takes care of that, I just added that as well. It looks for an element with the name `"items"`, and returns it as an array if it is found. – Bryan Feb 02 '16 at 19:33
  • @david.mihola I could try that. – Bryan Feb 02 '16 at 19:34
  • @david.mihola Didn't work :/ I still had to put a loop before the call to `ParcelableUtil.marshall()` for it to work. – Bryan Feb 02 '16 at 19:48
  • 1
    OK, so at least we know, that the `TypeAdapterFactory` is not to blame... :-) I don't really `Parcel` things that often... Have you seen this question: http://stackoverflow.com/questions/27253555/com-google-gson-internal-linkedtreemap-cannot-be-cast-to-my-class – david.mihola Feb 02 '16 at 19:51
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/102399/discussion-between-bryan-and-david-mihola). – Bryan Feb 02 '16 at 19:59

1 Answers1

0

Ah, ProGuard, the bane of my existence...

I dawned on me this morning. ProGuard could have been removing the class, and using the class in the for loop must have told ProGuard that the class was actually needed. I added the following to my ProGuard rules and it now works without cycling through a loop:

-keep public class com.example.common.model.MyObject
-keep public class * implements com.example.common.model.MyObject
-keepclassmembers class com.example.common.model.MyObject {
    <methods>;
}

I believe in my case I would only need the first line, but I think the other ones will just cover my bases.

Bryan
  • 14,756
  • 10
  • 70
  • 125