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.