Kotlin serializationでJSONのpolymorphicなオブジェクトをデシリアライズする

7 min read読了の目安(約7000字

はじめに

Kotlin serializationではGsonやMoshiといったAndroid開発ではデファクトとなっているJSONシリアライズ/デシリアライズライブラリと同様にpolymorphicなオブジェクトをデシリアライズするための方法が用意されています。
この記事ではKotlin serializationでpolymorphicなオブジェクトをデシリアライズするための方法をいくつか紹介します。

前提

この記事では以下のバージョンを前提としています

  • Kotlin serialization: 1.0.1
  • Kotlin 1.4.21

デシリアライズ対象

polymorphicなオブジェクト構造を持っているJSONとしてGeoJSONを考えます。

GeoJSON

GeoJSONのフォーマット詳細については他所を参照していただきたいのですが、今回は例として以下のような構造をデシリアライズすることを考えます。

{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {},
            "geometry": {
                "type": "Point",
                "coordinates": [10, 40]
            }
        },
        {
            "type": "Feature",
            "properties": {},
            "geometry": {
                "type": "MultiPoint",
                "coordinates": [
                    [10, 40], [40, 30], [20, 20], [30, 10]
                ]
            }
        },
        {
            "type": "Feature",
            "properties": {},
            "geometry": {
                "type": "GeometryCollection",
                "geometries": [
                    {
                        "type": "Point",
                        "coordinates": [10, 40]
                    },
                    {
                      "type": "MultiPoint",
                      "coordinates": [
                          [10, 40], [40, 30], [20, 20], [30, 10]
                      ]
                    }
                ]
            }
        }
    ]
}

GeoJSONでは FeatureCollection オブジェクトの中に Feature オブジェクトの配列を、さらに Feature オブジェクトの中には geometry が定義されています。
この geometryPoint, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon というgeometryを記載することが可能です。
また、これらgeometryの集合である GeometryCollection というオブジェクトをサポートしています。

デシリアライズ可能クラスの定義

まずはこのJSONをKotlin serializationでシリアライズ/デシリアライズするためのクラス定義をしていきます。

@Serializable
data class FeatureCollection(
    @SerialName("type")
    val type: String,
    @SerialName("features")
    val features: List<Feature>
)

@Serializable
data class Feature(
    @SerialName("type")
    val type: String,
    @SerialName("geometry")
    val geometry: Geometry,
    @SerialName("properties")
    val properties: Properties
)

@Serializable
data class Properties(
    // 省略
)

@Serializable
sealed class Geometry {
    @Serializable
    data class GeometryCollection(
        @SerialName("type")
        val type: String,
        @SerialName("geometries")
        val geometries: List<Geometry>
    ) : Geometry()

    @Serializable
    data class Point(
        @SerialName("type")
        val type: String,
        @SerialName("coordinates")
        val coordinates: List<Double>
    ) : Geometry()

    @Serializable
    data class MultiPoint(
        @SerialName("type")
        val type: String,
        @SerialName("coordinates")
        val coordinates: List<List<Double>>
    ) : Geometry()
}

FeatureCollection, Feature クラスの構造はJSON構造そのままなので特に解説はしません。また、 Properties クラスに関しては後ほど解説するため一旦省略します。
Featuregeometry として定義可能な Point, MultiPoint, GeometryCollection などは今回 Geometry クラスのsealed classとして定義していますが、 Geometry をインターフェースとして実装することも可能です。

typeをプロパティとして持つJSONオブジェクトのデシリアライズ

今回、 geometry として定義される各オブジェクトはプロパティに type を持っています。
Kotlin serializationではこのような type によってpolymorphismが表現されるJSONオブジェクトはクラスに対して @SerialName を付与し type プロパティの値を指定するだけでデシリアライズすることが可能です。他に設定や実装は必要ありません。

したがって、 Geometry クラスの実装は以下のようになります。

@Serializable
sealed class Geometry {
    @Serializable
+   @SerialName("GeometryCollection")
    data class GeometryCollection(
        @SerialName("type")
        val type: String,
        @SerialName("geometries")
        val geometries: List<Geometry>
    ) : Geometry()

    @Serializable
+   @SerialName("Point")
    data class Point(
        @SerialName("type")
        val type: String,
        @SerialName("coordinates")
        val coordinates: List<Double>
    ) : Geometry()

    @Serializable
+   @SerialName("MultiPoint")
    data class MultiPoint(
        @SerialName("type")
        val type: String,
        @SerialName("coordinates")
        val coordinates: List<List<Double>>
    ) : Geometry()
}

オブジェクトの内容によってデシリアライズ先を決める

geometrytype プロパティで型を決定できるため @SerialName を付与するだけでうまくデシリアライズすることができました。
では、 type プロパティを持っていないなどでこの方法が使えないときはどうしたらいいでしょうか。

ここでは、GeoJSONのユーザ定義オブジェクトである properties を例に考えてみます。
Featureproperties として以下のオブジェクトがあるものとして考えます。

{
    "type": "Feature",
    "properties": {
        "name": "Camera shop",
        "shop_id": "100"
    },
    "geometry": {} // 省略
},
{
    "type": "Feature",
    "properties": {
        "name": "My favorite bakery",
        "shop_id": "1012",
        "mylist_name": "food",
        "mylist_id": "29991"
    },
    "geometry": {} // 省略
}

properties には nameshop_id だけを持った店舗プロパティ、店舗プロパティに追加で mylist_namemylist_id を持ったマイリストプロパティがあるものとします。
クラスとして表すと次のようになります。

@Serializable
sealed class Properties {
    @Serializable
    data class ShopProperties(
        @SerialName("name")
        val name: String,
        @SerialName("shop_id")
        val shopId: Long
    ) : Properties()

    @Serializable
    data class MyListShopProperties(
        @SerialName("name")
        val name: String,
        @SerialName("shop_id")
        val shopId: Long,
        @SerialName("mylist_name")
        val myListName: String,
        @SerialName("mylist_id")
        val myListId: Long
    ) : Properties()
}

このような場合、JSONオブジェクトの内容を見てデシリアライズ先を振り分けるシリアライザを実装する必要があります。
こういった用途のシリアライザは JsonContentPolymorphicSerializer を実装することで簡単に作ることができます。

object PropertiesSerializer :
    JsonContentPolymorphicSerializer<Properties>(Properties::class) {

    override fun selectDeserializer(
        element: JsonElement
    ): DeserializationStrategy<out Properties> {
        return if (element.jsonObject.keys.any { it == "mylist_id" }) {
            Properties.MyListShopProperties.serializer()
        } else {
            Properties.ShopProperties.serializer()
        }
    }
}

このシリアライザを先程の Propeties クラスに指定します。

-@Serializable
+@Serializable(PropertiesSerializer::class)
 sealed class Properties {
     @Serializable
     data class ShopProperties(
         @SerialName("name")
         val name: String,
         @SerialName("shop_id")
         val shopId: Long
     ) : Properties()
     
     // 省略
 }

これでJSONオブジェクトの内容に応じたデシリアライズ先の振り分けが実装できました。

おわりに

本記事ではKotlin serializationを使ったpolymorphicなオブジェクトをデシリアライズする方法をいくつか紹介しました。
この記事で紹介した以外にもpolymorphismを扱う方法がいくつか用意されているため、詳しく知りたい方は参考文献の2や3を参照してみてください。

参考文献:

  1. GIS実習オープン教材
  2. Polymorphism
  3. Content-based polymorphic deserialization