この記事は、 Android Advent Calendar 2020の23日目の記事になります。
はじめに
- みなさんはAndroid開発のなかでjson parserは何を使われていますか?
- 多そうなところですと、moshiを利用されているプロジェクトが多そうですよね。
- 今年の10月にJetBrains公式のkotlinx.serializationが1.0.0版としてリリースされました。
- kotlinx.serialization も使用候補に上がってくるかなと思っています。
なにをするか
- この記事では、kotlinx.serializationを利用して、jsonの配列データのパース実装時に、階層クラスに置き換えるという対応をしてみようと思います。
確認環境
- Android Studio 4.1.1
- Kotlin version 1.4.20
導入準備
- Project build.gradle
build.gradle
buildscript {
ext.kotlin_version = '1.4.10'
repositories { jcenter() }
dependencies {
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}
- Module or App build.gradle
build.gradle
apply plugin: 'kotlin'
apply plugin: 'kotlinx-serialization'
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
}
使用するjsonデータ
weaponList.json
{
"data": {
"weapon_items": [{
"type": "Sword",
"name": "Stone-Sword",
"damage": 5,
"material": "Cobblestone"
}, {
"type": "Axe",
"name": "Gold-Axe",
"damage": 7,
"material": "Gold"
}, {
"type": "Sword",
"name": "Diamond-Sword",
"damage": 7,
"material": "Diamond"
}, {
"type": "Bow",
"name": "Bow",
"damage": 9,
"infinity": false
}, {
"type": "Bow",
"name": "InfinityBow",
"damage": 9,
"infinity": true
}, {
"type": "Trident",
"name": "Trident",
"damage": 9,
"loyalty": true,
"channeling": false,
"riptide": false
}]
}
}
- マインクラフトの武器がリストで並んでいるデータです。
- なんのことだと思った方はコチラをどうぞ
Modelデータ
weaponModel.kt
@Serializable
sealed class Weapon{
abstract val name: String
abstract val damage: Int
}
@Serializable
@SerialName("Sword")
data class Sword(
override val name: String,
override val damage: Int,
val material: String
) : Weapon()
@Serializable
@SerialName("Axe")
data class Axe(
override val name: String,
override val damage: Int,
val material: String
) : Weapon()
@Serializable
@SerialName("Trident")
data class Trident(
override val name: String,
override val damage: Int,
val loyalty : Boolean,
val channeling : Boolean,
val riptide : Boolean
) : Weapon()
@Serializable
@SerialName("Bow")
data class Bow(
override val name: String,
override val damage: Int,
val infinity : Boolean
) : Weapon()
@Serializable
@SerialName("Data")
data class Data(
@SerialName("weapon_items")
val reportItems: List<Weapon>
)
@Serializable
@SerialName("Response")
data class Response(
@SerialName("data")
val data: Data
)
-
kotlinx.serialization の記述方法などはコチラを参考にしてみましょう。
-
@Serializable
は自動的にシリアライザクラスを作成するためのアノテーションです。 -
@SerialName("XXX")
はjsonのキー名と、プロパティー名、クラス名を紐付けるためのアノテーションです。
-
Weapon
というsealed class
を用意し、それを継承した、Sword
、Axe
、Trident
、Bow
という武器種のクラスを作成しています。
変換する為には
- 階層クラスを扱うためには Polymorphism の仕組みを利用します。
階層クラスの登録
-
SerializersModuleBuilder
を利用し、polymorphic
へ継承クラス、subclass
に実際のクラスを登録します。
weapon.kt
val weaponModule = SerializersModule {
polymorphic(Weapon::class) {
subclass(Sword::class)
subclass(Axe::class)
subclass(Trident::class)
subclass(Bow::class)
}
}
変換(デコード)処理
weaponTeste.kt
val testJsonString = "{\"data\":{\"weapon_items\":[{\"type\":\"Sword\",\"name\":\"Stone-Sword\",\"damage\":5,\"material\":\"Cobblestone\"},{\"type\":\"Axe\",\"name\":\"Gold-Axe\",\"damage\":7,\"material\":\"Gold\"},{\"type\":\"Sword\",\"name\":\"Diamond-Sword\",\"damage\":7,\"material\":\"Diamond\"},{\"type\":\"Bow\",\"name\":\"Bow\",\"damage\":9,\"infinity\":false},{\"type\":\"Bow\",\"name\":\"InfinityBow\",\"damage\":9,\"infinity\":true},{\"type\":\"Trident\",\"name\":\"Trident\",\"damage\":9,\"loyalty\":true,\"channeling\":false,\"riptide\":false}]}}"
val format = Json { serializersModule = weaponModule }
val weaponResponse = format.decodeFromString<Response>(testJsonString)
変換結果
- weaponResponse の値を
toString()
した結果 - jsonの並び順や、クラス定義などが正しく変換されて取得できました。
Response(data=Data(
reportItems=[
Sword(
name=Stone-Sword,
damage=5,
material=Cobblestone),
Axe(
name=Gold-Axe,
damage=7,
material=Gold),
Sword(
name=Diamond-Sword,
damage=7,
material=Diamond),
Bow(
name=Bow,
damage=9,
infinity=false),
Bow(
name=InfinityBow,
damage=9,
infinity=true),
Trident(
name=Trident,
damage=9,
loyalty=true,
channeling=false,
riptide=false)
]
))
注意点
jsonデータとModelデータの中で注意しないとならない点があります。
Stone-Swordのjsonデータ
Stone-Sward.json
{"type":"Sword","name":"Stone-Sword","damage":5,"material":"Cobblestone"}
- Sword のModelデータ
weaponModel.kt
@Serializable
@SerialName("Sword")
data class Sword(
override val name: String,
override val damage: Int,
val material: String
) : Weapon()
- 上のjsonデータとModelデータで差があるのは
type
の箇所がModelデータに存在していないという点があります。 - これは
Polymorphism
の仕組みの中でtype
というキーが、クラスの判別のために利用されているからになります。 - そのためModelデータに
type
を取得する為にabstract val type: String
をプロパティーとして追加した場合、以下のExceptionが発生します。- クラスに振り分けられるので、
type
を保持する必要も無いかなと思いますが - jsonの
type
というキーを別の用途に利用したいなどには、この実装は使えないという問題点があります
- クラスに振り分けられるので、
java.lang.IllegalArgumentException: Polymorphic serializer for class Sword (Kotlin reflection is not available) has property 'type' that conflicts with JSON class discriminator. You can either change class discriminator in JsonConfiguration, rename property with @SerialName annotation or fall back to array polymorphism
おわりに
- kotlinx.serializationを利用した、配列データの階層クラスパースについて書かせていただきました。
- 配列データを階層クラスとして扱いたい場合に、変換(デコード)時点で適切なクラスに振り分けられるので非常に扱いやすい状態が作れます。
- この挙動を理解するためには、クラスからjsonに変換するエンコード処理を書いてみると理解しやすいかなと思いますので、興味のある方はぜひ試して見てください
これからも楽しいアプリケーション開発を