前置き
Kotlin/JVMやAndroid界隈ではJsonパーサーとして古くはGsonが主流でしたが、最近ではKotlinSerializationが採用されることが多くなってきたと思います。
それ以外にもJacksonやMoshiなど数多くのパーサーが存在します。
そして最もJsonパーサーが活用される場面といったらAPIレスポンスのモデルマッピングではないでしょうか?
API通信用のクライアントライブラリでは今も昔もRetrofitが採用されることが多いかと思います。
そこで今回はRetrofitを用いたJsonパーサーを少しずつ移行する方法を紹介します。
よくある話
よくあるケースとして、歴史が積み上がってきたアプリケーションでは
Retforit+Gson(など)をRetforit+KotlinSerializationに移行したいという要求でてくることがあります。
しかしながら、今まで数多く積み上げてきたGsonのレスポンスモデルをまとめて移行するのが億劫で二の足を踏んでる方もいるでしょう。
一括置換しようにもKotlinのnullableやデフォルト値、data classの扱いがライブラリごとに違って機械的な置き換えができない場合もあります。
また、レスポンスモデルごとにRetrofitを2つにわけるという手もありますが、対応するRetrofitを間違えても実行するまで気づきにくくヒューマンエラーの元にもなります。
data class ResponseModel(
@SerializedName("name")
val name: String,
)
@Serializable
data class ResponseModel(
@SerialName("name")
val name: String,
)
そこで1つのRetrofit内でレスポンスモデルがGsonかKotlinSerializationかを判定してどちらにパースをかけるべきか判断する手段を紹介します。
このような手段を設けることでレスポンスモデルを一つずつ安全に移行していくことができます。
今回はGson->KotlinSerializationへの移行をベースに解説しますが、移行元と移行先が別の技術だとしても紹介する方法と近いやり方で区別する事が可能です。
各種Converterのラッパーを書く
RetrofitでJsonパーサーを挟むにはConverterを噛ませます。
https://github.com/square/retrofit/tree/trunk/retrofit-converters
本来であればGsonやKotlinSerialization向けのConverterを直接追加しますが、今回は分岐処理を設けたラッパーConverterを書きます。
class JsonConverterFactory : Converter.Factory() {
// 移行元と移行先のコンバーターを定義
private val kotlinSerializationFactory = Json.asConverterFactory("application/json".toMediaType())
private val gsonFactory = GsonConverterFactory.create()
override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>, retrofit: Retrofit): Converter<ResponseBody, *>? {
val rawType = getRawType(type) // レスポンスモデルとして指定されているクラスを取り出す
rawType.annotations.forEach { annotation ->
if (annotation.annotationClass == Serializable::class) {
// クラスにSerializableアノテーションが付いている場合はKotlinSerialization向けと見なしてそちらに処理に流す
return kotlinSerializationFactory.responseBodyConverter(type, annotations, retrofit)
}
}
// それ以外はGson向けと見なしてそちらに処理に流す
return gsonFactory.responseBodyConverter(type, annotations, retrofit)
}
override fun requestBodyConverter(type: Type, parameterAnnotations: Array<out Annotation>, methodAnnotations: Array<out Annotation>, retrofit: Retrofit): Converter<*, RequestBody>? {
val rawType = getRawType(type) // レスポンスモデルとして指定されているクラスを取り出す
rawType.annotations.forEach { annotation ->
if (annotation.annotationClass == Serializable::class) {
// クラスにSerializableアノテーションが付いている場合はKotlinSerialization向けと見なしてそちらに処理に流す
return kotlinSerializationFactory.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit)
}
}
// それ以外はGson向けと見なしてそちらに処理に流す
return gsonFactory.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit)
}
override fun stringConverter(type: Type, annotations: Array<out Annotation>, retrofit: Retrofit): Converter<*, String>? {
val rawType = getRawType(type) // レスポンスモデルとして指定されているクラスを取り出す
rawType.annotations.forEach { annotation ->
if (annotation.annotationClass == Serializable::class) {
// クラスにSerializableアノテーションが付いている場合はKotlinSerialization向けと見なしてそちらに処理に流す
return kotlinSerializationFactory.stringConverter(type, annotations, retrofit)
}
}
// それ以外はGson向けと見なしてそちらに処理に流す
return gsonFactory.stringConverter(type, annotations, retrofit)
}
}
そしてこれを1つのConverterとしてRetrofitに噛ませます。
val retrofit = Retrofit.Builder()
...
.addConverterFactory(JsonConverterFactory())
.build()
おしまい
このようにすることでConverterを意識することなく、レスポンスモデル単位でGson->KotlinSerializationへ移行することができ、どちらの技術でパースするかも自動的にRetrofit内で判断してくれるようになります。
一度作り始めれば簡単なので、Jsonパーサーを移行したいモチベーションはあるが一斉置換は避けたいというプロダクトでは最初にこの手のラッパーConverterを用意して、少しずつ技術を差し替えて移行していくことをおすすめします。