はじめに
Kotlin Serialization の継承クラスの挙動を確認しようとしたところ、継承先のクラスで Serializable を有効にできない事象が発生しました。
@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年に報告されていますが解決していないようです。
発生環境
- kotlin 2.0.21
- kotlinx-serialization-json 1.7.2
なぜなのか
この現象の解決が困難であることについて考察してみました。
プロパティ override はどのように実現されているか
ます、プロパティ override の成り立ちはどのようになっているかというと、フィールド getter メソッドの override によって実現されています。
これは Java には無い機能ですが、もし Java で記述すると下記のようになります。
public class Parent {
private final String name = "Parent";
public String getName() {
return name;
}
}
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
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
}
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
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
}
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
に ダウンキャストすることはできない。
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 ダウンキャスト不可
}