[kotlin] serialVersionUID keeps changing after setting rules to keep

Here’s my rule for serialized classes:

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

And here’s how I set serialVersionUID:

User(string1:String,int:Int,string2:String):Serializable,UpdatableUser(string1,int,string2) {
    companion object {
        private const val serialVersionUID: Long = 111L
    }

Every time I change things (outside of the User or UpdatableUser classes), I see that the saved objects can’t be deserialized because the classdesc serialVersionUID and local class serialVersionUID are different.

I’m thinking this has something to do with how companion objects in Kotlin appear in JVM. Like I get that strictly speaking companion is Kotlin’s answer to static but it doesn’t look exactly the same to the JVM under the hood. And I can’t use @JvmStatic for things other than methods, so that’s out.

Can anyone tell me how I can make the serialVersionUID stay the same and not get obfuscated?

Ok, so I didn’t think to look in usage.txt before, but after I did, it made it pretty clear that every companion object had the assigned name of “Companion”. So I added *** Companion; to the keep rule for serializable classes.

After that, usage.txt stopped indicating they were being stripped. It still looks from the mappings like they’re being obfuscated. I’m guessing, very naively, that under the hood the long that I’m trying to persist is being wrapped inside of an object called Companion and what I’m looking at inside the mappings isn’t the long but the init method for that object, which ultimately holds the long.

Anyway, the symptom isn’t happening anymore. And if it comes back in a few days, at least I know where to look to figure out why.

Even though the solution was mostly just to look in usage.txt, I’m leaving the question and my answer since there really isn’t much here or on stackexchange about kotlin+proguard+serialized classes.

1 Like

Hi @b0b4nd7 !

It sounds like you’ve solved your problem, great!

Indeed, when Kotlin companion objects are compiled into a separate class named either `Companion`` by default or the given name of the companion object e.g.

class Foo {
    companion object { }
}

Generates Foo.class and Foo$Companion.class

class Foo {
    companion object Bar { }
}

Generates Foo.class and Foo$Bar.class

As you already discovered you will have to adjust your -keep rules to take this into account.

When using private const val in the companion object, a field will be added in the containing object e.g.

User(string1:String,int:Int,string2:String):Serializable,UpdatableUser(string1,int,string2) {
    companion object {
        private const val serialVersionUID: Long = 111L
    }
}

Should generate Foo.class with the serialVersionUID field and Foo$Companion.class.

You might find the ProGuard Playground useful for this kind of problem - you can upload your jar/apk, try out -keep rules and immediately see the entities that match the rules https://playground.proguard.com/

1 Like

I don’t see a way to edit my first response above, so I’ll just put this here because I think it’s pretty useful information for people struggling with persisting serialized objects.

I kept getting confounding errors even though it was obvious that my serialVersionUIDs were not being obfuscated. So I did a deep dive and got ahold of one of the serialized objects right off of the phone disk and tried to read them as best I could in UTF-8 to see if I could get any clues, and sure enough the mapping l.lc, which corresponded to the User class, was referenced explicitly by name inside of the file, consistent with grammar laid out in oracle’s docs on serialization. And that’s because I don’t actually serialize my state objects themselves per se, I serialize containers for those objects.

And that difference can create really confusing errors. If you have a serializable container class Container() and it has a member list of serializable objects, those objects will be referenced by class name inside of the serialized container. The punchline being that regardless of how much you protect the serialVersionUID, if the mapping name of the classes held inside of Container() has changed between serializing and deserializing, you’ll be stuck trying to cast an object of type old.mapping to an instance of type new.mapping.

I guess this doesn’t come up a lot because an ordinary person is probably going to persist their object and not a container for that object.

I don’t have time in the immediate term to refactor the persistence scheme. So the only way I thought right to fix it was to just put a blanket keep on all the serialized classes. Kind of a bummer because there’s a lot in there.

Anyway, I’m just putting this here in case it could help someone else because I had a really difficult time with it. If you have serializable objects inside of a serializable container, and you persist the container, the objects inside will be labeled as their mapping name. And that mapping name is likely to change.

EDIT:

Ok so I really hate that I’m hijacking this as my personal blog/therapy session but I’m just trying to put myself in the shoes of someone who googled “kotlin serialized classes proguard” and ended up here.

I was really bummed out about putting a huge -keep over all my persistent classes because even though there’s nothing sensitive in there, it still seems kind of unprofessional looking to just leave so much business logic in plaintext. And I built so much on top of Serializable that adopting another persistence strategy wasn’t the solution I wanted even if I may decide to eventually.

TLDR: I just decided to use -applymapping so that even though the persisted classes got new names, at least the mappings wouldn’t change.

Longer explanation in case you’re also thinking of trying something different:

For testing I found a nice big chunk of kind of irrelevant stuff that I could comment/uncomment in order to consistently change mappings between builds.

In order to avoid versioning issues with Java’s ObjectInputStream, I’ve always organized my persistent classes into couples of the form UpdatableClassName(), which holds the serialVersionUID and all the real substance of the class, and ClassName() which extends UpdatableClassName and only holds the serialVersionUID and the readObject() function. That way I can persist across updates by putting any new changes inside of ClassName and patch up the difference with readObject().

I thought that maybe I could only apply the keep rule to ClassName() and then keep all of the business logic hidden inside of UpdatableClassName and just let the mapping change since only ClassName is explicitly referenced. But that doesn’t work because as readObject() reads up the graph, it finds the mapping for UpdatableClassName (which has changed between builds) and throws an exception.

Then I considered intercepting the stream from readObject() and then changing the class name at runtime. But by that point, you’re already so deep in the weeds that it’s kind of a silly idea even if ObjectInputStream made it easy to do, which it doesn’t. I only found one example of someone achieving that and they had the benefit of knowing was the old name was, which I won’t.

So then I went through the docs and found out that -applymapping already exists and this use case seemed like a good candidate for it.

That’s it. Final update. Feel free to opine with why this isn’t a good use case for -applymapping or why ObjectInputStream is a bad way to persist state on android.

4 Likes