Not possible to serialize object singleton in Kotlin

Viewed 707

One common uses for sealed classes, in Kotlin, is to have a combination of cases that have some data (using data class) and others that are singletons (using object) like this:

sealed class Location {
    object Unknown : Location()
    data class Known(val lat: Float, val lon: Float) : Location()
}

The system we are using requires the model to implement Serializable. To my surprise, this doesn't work with object singletons as you can see on this demo:

https://pl.kotl.in/vd_TgUR6a

Which outputs the following:

Success: Known(lat=37.563934, lon=-116.85123) and Known(lat=37.563934, lon=-116.85123) are equal
Failure: sample.Location$Unknown@7c3df479 and sample.Location$Unknown@452b3a41 are not equal

The instance id is different. My guess is that the JVM deserializes it using "artifitial" means. Be it Reflexion or another synthetic way.

How can I make this work?

1 Answers

I found out a "bester" solution. Using the hidden JVM API of readResolve():

sealed class Location: Serializable {
    object Unknown : Location() {
        private fun readResolve() : Any? = Location.Unknown
    }
    data class Known(val lat: Float, val lon: Float) : Location()
}

Code here: https://pl.kotl.in/tMGf-AIfq

Produces the following output:

Success: Known(lat=37.563934, lon=-116.85123) and Known(lat=37.563934, lon=-116.85123) are equal
Success: sample.Location$Unknown@452b3a41 and sample.Location$Unknown@452b3a41 are equal

This function is called after an object is loaded from a stream and allows to return a different object instead of the one loaded from memory.

This means that it's safe to use even if our object has a state (not that it should though. And should be much more memory efficient.

There is a ticket about this here: https://youtrack.jetbrains.com/issue/KT-9499

And it looks like it may be coming as part of this other ticket to add the @JVMSerializable annotation: https://youtrack.jetbrains.com/issue/KT-14528

Old Answer:

The best solution I found was to make the singleton object override the default equals, hashCode and toString to make it functionally speaking identical:

sealed class Location: Serializable {
    object Unknown : Location() {
        override fun equals(other: Any?) = other is Unknown
        override fun hashCode() = toString().hashCode()
        override fun toString(): String = "Location.Unknown"
    }
    data class Known(val lat: Float, val lon: Float) : Location()
}

Here is the demo: https://pl.kotl.in/oNd-mnWlQ

And the output is:

Success: Known(lat=37.563934, lon=-116.85123) and Known(lat=37.563934, lon=-116.85123) are equal
Success: Location.Unknown and Location.Unknown are equal

This is a possible solution if memory is not an extreme concern as it will create an object for each of the deserialized objects. Albeit, the object has a very small footprint, which shouldn't be a concern for most cases but something to be aware of.

This wouldn't have been a problem if Kotlin could have implemented object using enum classes as they are serialized differently in the JVM. However, they can't extend classes (only interfaces) so it wouldn't work for this instance.

All that said Oracle plans to drop it eventually: https://www.infoworld.com/article/3275924/oracle-plans-to-dump-risky-java-serialization.html

In the meantime, we are stuck with this.

Note: This works as long as the object is not mutable. If we allow changing states all hell breaks open as we start having different states on each deserialised object.

Related