kotlinx.serializationを使って配列+階層クラスのパースを実施するには（注意点有り）

この記事は、 [Android Advent Calendar 2020](https://qiita.com/advent-calendar/2020/android)の23日目の記事になります。

# はじめに

- みなさんはAndroid開発のなかでjson parserは何を使われていますか？
- 多そうなところですと、moshiを利用されているプロジェクトが多そうですよね。
- 今年の10月にJetBrains公式の[kotlinx.serialization](https://github.com/Kotlin/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
		}]
	}
}

```

- マインクラフトの武器がリストで並んでいるデータです。
- なんのことだと思った方は[コチラ](https://w.atwiki.jp/minecraft/pages/35.html)をどうぞ

## 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 の記述方法などは[コチラ](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md)を参考にしてみましょう。
    - `@Serializable`は自動的にシリアライザクラスを作成するためのアノテーションです。
    - `@SerialName("XXX")`はjsonのキー名と、プロパティー名、クラス名を紐付けるためのアノテーションです。

- `Weapon`という`sealed class`を用意し、それを継承した、` Sword`、` Axe`、` Trident`、` Bow`という武器種のクラスを作成しています。

## 変換する為には

- 階層クラスを扱うためには [Polymorphism](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#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)
    ]
))

```

## 注意点:zap:
- 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`を保持する必要も無いかなと思いますが:sweat_smile:
    - jsonの`type`というキーを別の用途に利用したいなどには、この実装は使えないという問題点があります:dizzy_face:

```　

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に変換するエンコード処理を書いてみると理解しやすいかなと思いますので、興味のある方はぜひ試して見てください:grinning:


これからも楽しいアプリケーション開発を:dancers: