Kotlin object + Serialization might cause bugs

Prashant Pol
4 min readJul 8, 2023

--

Sometimes things looks simple and we use it many times but there migth some tweaks associated with it which might cause bugs in certain situations.

Today, we are going to see one such thing. Which is combo of kotlin objects and java serialization.

Kotlin object

Kotlin simplified management of singleton classes and its objects.

// Kotlin
object Hello

vs

// Java
public final class Hello {
@NotNull
public static final Hello INSTANCE;

private Hello() {
}

static {
Hello var0 = new Hello();
INSTANCE = var0;
}
}

For singleton classes, object remains same, hence we can compare objects with equal operator `==`.
for example,

object Hello

val ob1 = Hello
val ob2 = Hello
assertTrue(ob1 == ob2)

In usual case we don’t need to compare singleton objects because it’s obviously same but when we have `object` as a
sub-type of `sealed` entity then there is a need to compare it.

sealed class Parent {

object ChildA : Parent()

data class ChildB(val data: Int) : Parent()
}

val parent1: Parent = ... // any child class
val parent2: Parent = ... // any child class

val isEqual = parent1 == parent2
// or
when (parent) {
ChildA ->
...
is ChildB ->
...
}

But above code won’t work always, if `Hello` or `Parent` is `Serializable`.
Let’s see how,

Problematic usage 1

// Hello.kt
object Hello : Serializable

// Extension function in some class.
fun Hello.clone(): Hello {
val byteArrayOs = ByteArrayOutputStream()
ObjectOutputStream(byteArrayOs).use {
it.writeObject(this)
}

return ObjectInputStream(ByteArrayInputStream(byteArrayOs.toByteArray())).use {
it.readObject() as Hello
}
}

// Use it somewhere
val original = Hello
val cloned = original.clone()
Log.i("TAG", "${original == cloned}")

Even IDE will show `original == cloned` is always true.
But in reality its different objects.

Problematic usage 2

`savedInstanceState` helps us to survive when activity recreates by system cause.

class MyActivity : AppCompatActivity() {
private var globalObj: Hello? = null

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)

globalObj = savedInstanceState?.getSerializable("key-1", Hello::class.java) ?: Hello
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)

outState.putSerializable("key-1", globalObj)
}

override fun onResume() {
super.onResume()

val localObj = Hello
Log.i("TAG", "Resume global=$globalObj and local=$localObj. Let's Equate ${globalObj == localObj}")
}
}

// Hello.kt
object Hello : Serializable

Let’s analyse this code in different scenario.

  • On configuration change (orientation, keyboard, locale, …)
    - Activity gets recreated but singleton object still remains same (localObj == globalObj)
    - Specify `android:configChanges` : Activity won’t be recreated (localObj == globalObj)
  • On runtime permission changes from system settings
    - Certain permission changed to `Allow`: after coming back to app, localObj == globalObj
    - Certain permission changed to `Deny`: after coming back to app, localObj != globalObj

Why???

Because user forcefully `Deny` certain permission, then app needs to handle it gracefully, to make app to handle
permission change,
entire app process will be killed and started again when user visits app again.

This caused kotlin `object` to generate its new object reference (with different hashcode).
But all this thing happened due to system caused reason, which makes savedInstance usable with old reference of `object`.

What does it cause?

  1. When equating two objects:
    Certainly it will fail and `==` returns `false`. Which is a kind of silent bug.
  2. When using in `when` statement:
    After retrieving from savedInstance or deserialization if we use such object in `when` then it will fail with `NoWhenBranchMatchedException`.
sealed class Parent : Serializable {
object ChildObject : Parent()

data class ChildClass(a: Int) : Parent()
}

// after deserialization
when (obj) {
ChildObject ->
... // this will fail

is ChildClass ->
...
}

How to resolve it?

1. Use `is` over `==`

This might look absurd but when using `object` + `Serialization` combo then better to use `is`.

sealed class Parent : Serializable {
object ChildObject : Parent()

data class ChildClass(a: Int) : Parent()
}

when (obj) {
is ChildObject ->
...

is ChildClass ->
...
}

Definitely additional comment is required for better understandability.

2. Override equals method of object

This is same as above solution, just we don’t need to mention `is` in every `when` block, developers might forget about it sometimes.

object Hello : Serializable {
override fun equals(other: Any?): Boolean = other is Hello
}

// You got saved yourself from using `is` all times
when (obj) {
ChildObject ->
...

is ChildClass ->
...
}

3. Use `Parcelable` over `Serializable`

Better solution when using in android. As Parcelable is lightweight it will be better solution while dealing with savedInstanceState.

4. Use `readResolve` in `object`

object Hello : Serializable {
private fun readResolve(): Any = Hello
}

function can be private or non-private but return type must be `Any`.

TL;DR

Whenever we see kotlin object + Serializable then treat it as red flag and take extra caution with any of above solution.

--

--