【Kotlin Serialization】子クラスでプロパティ override すると Serializable にできない

# はじめに

Kotlin Serialization の継承クラスの挙動を確認しようとしたところ、継承先のクラスで Serializable を有効にできない事象が発生しました。

``` kotlin
@Serializable
open class Parent {
    open val name: String = "Parent"
}

@Serializable // <-- NG
class Child : Parent() {
    override val name: String = "Child"
}
```

```
Error when overriding property: serializable class has duplicate serial name of property 'name', either in the class itself or its supertypes
```

これは既知の問題で、すでに2020年に報告されていますが解決していないようです。

https://youtrack.jetbrains.com/issue/KT-38958

# 発生環境

- kotlin 2.0.21
- kotlinx-serialization-json 1.7.2

# なぜなのか

この現象の解決が困難であることについて考察してみました。

#### プロパティ override はどのように実現されているか

ます、プロパティ override の成り立ちはどのようになっているかというと、フィールド getter メソッドの override によって実現されています。
これは Java には無い機能ですが、もし Java で記述すると下記のようになります。

``` Java
public class Parent {
    private final String name = "Parent";

    public String getName() {
        return name;
    }
}
```
``` Java
public class Child extends Parent {
    private final String name = "Child";

    @Override
    public String getName() {
        return name;
    }
}
```

`serializable class has duplicate serial name of property 'name'` なのが明確になったかと思います。

# 他のコンバータの挙動

Gson と Moshi で以下の挙動を確認してみます。

- Parent インスタンス化したオブジェクトを Json 文字列化して、オブジェクトへ戻す
- Child インスタンス化したオブジェクトを Json 文字列化して、オブジェクトへ戻す
- 変換処理後のダウンキャスト、ポリモーフィズムの確認

#### Gson の場合

- gson 2.11.0

``` Kotlin
    fun testParentGsonParse() {
        val gson = Gson()

        val parent = Parent()
        val json = gson.toJson(parent)
        println(json) // {"name":"Parent"}

        val parsedParent = gson.fromJson(json, Parent::class.java)
        println(parsedParent.name) // Parent
    }
```

``` Kotlin
    fun testChildGsonParse() {
        val gson = Gson()

        val child = Child()
        val json = gson.toJson(child) // フィールド名の重複で IllegalArgumentException
        println(json)
    }
```
Gson は同名のプロパティ名の問題を解決できない。


#### Moshi の場合

- ksp 2.0.21-1.0.28
- moshi-kotlin 1.15.2

``` Kotlin
    fun testParentMoshiParse() {
        val moshi = Moshi.Builder().build()
        val adapter = moshi.adapter<Parent>(Parent::class.java)

        val parent = Parent()
        val json = adapter.toJson(parent)
        println(json) // {}

        val parsedParent = adapter.fromJson(json)
        println(parsedParent?.name) // Parent
    }
```

``` Kotlin
    fun testChildMoshiParse() {
        val moshi = Moshi.Builder().build()
        val adapter = moshi.adapter<Child>(Child::class.java)

        val child = Child()
        val json = adapter.toJson(child)
        println(json) // {}

        val parsedChild = adapter.fromJson(json)
        println(parsedChild?.name) // Child
    }
```

同名のプロパティ名があっても失敗しない。
デフォルト値の不変プロパティは json に出力されない。

元がどのクラスでインスタンス化したかは関係なく、json から復元したオブジェクトは Adapter のクラス情報に基づいている。
`Child` インスタンスを json 化して、`Parent` の Adapter で復元すると `Parent` としてインスタンス化されたオブジェクトになるため、`Child` に ダウンキャストすることはできない。

``` Kotlin
    fun testChildParentMoshiPolymorphism() {
        val moshi = Moshi.Builder().build()
        val childAdapter = moshi.adapter<Child>(Child::class.java)

        val child = Child()
        val json = childAdapter.toJson(child)
        println(json) // {}

        val parsedByChild = childAdapter.fromJson(json)
        println(parsedByChild?.name) // Child

        val childPolymorphic = parsedByChild as? Parent
        println(childPolymorphic?.name) // Child

        // Parent型のAdapterで復元する
        val parentAdapter = moshi.adapter<Parent>(Parent::class.java)
        val parsedByParent = parentAdapter.fromJson(json)
        println(parsedByParent?.name) // Parent

        val parentPolymorphic = parsedByParent as? Child
        println(parentPolymorphic?.name) // null ダウンキャスト不可
    }
```