Kotlin serializationでJSONのpolymorphicなオブジェクトをデシリアライズする
はじめに
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
が定義されています。
この geometry
は Point
, 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
クラスに関しては後ほど解説するため一旦省略します。
Feature
の geometry
として定義可能な 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()
}
オブジェクトの内容によってデシリアライズ先を決める
geometry
は type
プロパティで型を決定できるため @SerialName
を付与するだけでうまくデシリアライズすることができました。
では、 type
プロパティを持っていないなどでこの方法が使えないときはどうしたらいいでしょうか。
ここでは、GeoJSONのユーザ定義オブジェクトである properties
を例に考えてみます。
各 Feature
の properties
として以下のオブジェクトがあるものとして考えます。
{
"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
には name
と shop_id
だけを持った店舗プロパティ、店舗プロパティに追加で mylist_name
と mylist_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を参照してみてください。
参考文献:
Discussion