2

Some time ago I published an app that serialized/deserialized an user object.

public String serializeUser(final User user) {
    final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    try {
        final ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(user);
        objectOutputStream.close();
    } catch (final IOException exception) {
        ...
    }

    return new String(Base64.encode(byteArrayOutputStream.toByteArray(), DEFAULT));
}

public User deserializeString(final String userString) {

    final byte userBytes[] = Base64.decode(userString.getBytes(), DEFAULT);
    final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(userBytes);

    final ObjectInputStream objectInputStream;
    final User user;
    try {
        objectInputStream = new ObjectInputStream(byteArrayInputStream);
        user = (User) objectInputStream.readObject();
        objectInputStream.close();
    } catch (final IOException | ClassNotFoundException exception) {
        ...
    }

    return user;
}

The object was implemented this way:

public class User implements Serializable {
    private String email;
    private String name;

    ...
}

Then, after modifying my object (I added a new field), I learned the hard way that one has to set the serialVersionUID in case the object definition ever changes, otherwise the deserializer won't be able to recognize the stored object (as it will autogenerate the serialVersionUID). So I went ahead and did just that:

public class User implements Serializable {
    private static final long serialVersionUID = 123L;

    ...
}

But now that I've republished the app with these changes, I keep getting error reports indicating the object could not be deserialized:

Caused by: java.io.InvalidClassException: com.myproject.h.e; local class incompatible: stream classdesc serialVersionUID = 184861231695454120, local class serialVersionUID = -2021388307940757454

I'm very aware that setting a new serial version would invalidate any previous serial version (link1, link2), but this isn't the case. As you can see the error log points to a totally different serialVersionUID (18486... and -20213...) than the one I manually set to my User class (123L).

What am I missing?

If it is of any relevance, I'm using Proguard with the default configs.

cavpollo
  • 4,071
  • 2
  • 40
  • 64
  • 1
    (FWIW, if you use the `serialver` command line program in the JDK, it will give you the serial version UID of a class. Even if you don't specify a specific value, the Java Serialization mechanism will compute one for you. Using the same UID allows old data to be read. Though if you have published multiple versions with slightly different public method (see docs for exact details), then they'll produce incompatible data.) – Tom Hawtin - tackline May 19 '19 at 12:20

1 Answers1

5

After googling for quite some time, I stumbled upon this question: How to stop ProGuard from stripping the Serializable interface from a class. So I went ahead and dove into the APK (one can analyze the APK in Android Studio), located my User class, and took a look at its Bytecode:

class public Lcom/myproject/h/e;
.super Ljava/lang/Object;
.source "User.java"

# interfaces
.implements Ljava/io/Serializable;


# annotations
...


# instance fields
.field private a:Ljava/lang/String;

.field private b:Ljava/lang/String;

...


# direct methods
...

As you can see, the Static field serialVersionUID is nowhere to be found. So I went ahead and added the following configuration as the documentation suggests:

[Applications] may contain classes that are serialized. Depending on the way in which they are used, they may require special attention. [...] Sometimes, the serialized data are stored, and read back later into newer versions of the serializable classes. One then has to take care the classes remain compatible with their unprocessed versions and with future processed versions. In such cases, the relevant classes will most likely have serialVersionUID fields. The following options should then be sufficient to ensure compatibility over time:

-keepnames class * implements java.io.Serializable`

-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

So, if I go to the newly generated APK and check my object's bytecode, it now specifies that the serialVersionUID is not to be discarded or renamed:

class public Lcom/myproject/model/User;
.super Ljava/lang/Object;
.source "User.java"

...

# static fields
.field private static final serialVersionUID:J


# instance fields
...

I would expect the app not to have any trouble deserializing my object anymore.

cavpollo
  • 4,071
  • 2
  • 40
  • 64
  • You are my hero. This issue has caused lot of troubles. My current version of data is serialized that no longer can be deserialized due to this incredibly annoying carelessness by pro-gaurd. I am looking at alternative solutions, I will post answer if I find way out but right now I am stuck with my upgrades as it will no longer deserialize currently serialized objects. To make matter worse my serialzed data is encrypted. – MG Developer Jun 26 '22 at 22:48