12
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-22

この記事は、 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を用意し、それを継承した、SwordAxeTridentBowという武器種のクラスを作成しています。

変換する為には

  • 階層クラスを扱うためには 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:

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6