retrofit2とmoshiとenum


若干詰まったので、備忘録

やりたかったこと

retrofit+moshiで、APIのレスポンス(json)をenumとしてデシリアライズして返してほしい

例:月の和名を返すAPI

アプリ側から月(1~12)をサーバにリクエストすると、レスポンスとして月の和名が返却されるような連携があり、
アプリ側とサーバ側の双方で、返却値としてのEnumが定義されている前提。

定数
MUTSUKI 0
KISARAGI 1
YAYOI 2
UDUKI 3
SATSUKI 4
MINADUKI 5
FUMIDUKI 6
HADUKI 7
NAGATUKI 8
KANNADUKI 9
SHIMOTUKI 10
SHIWASU 11
UNKNOWN 12

フォーマット

// リクエスト
{
 "month": 1
}

// レスポンス(名前で返す)
{
 "month_jpn": "MUTSUKI"
}

// レスポンス(序数で返す)
{
 "month_jpn": 0
}

アプリ側

// Enumの定義
enum class Wareki {
    // 縦に長いので折り返してます
    MUTSUKI, KISARAGI, YAYOI, UDUKI,
    SATSUKI, MINADUKI, FUMIDUKI, HADUKI,
    NAGATUKI, KANNADUKI, SHIMOTUKI, SHIWASU,
    UNKNOWN
}

// レスポンスを受け取るオブジェクトのクラス
data class ResData(
    @Json(name = "month_jpn") 
    val monthJpn: Wareki
)

// APIをコールする箇所
suspend fun fetchMonthJpn(month: Int) {
    try {
        // ここ
        val res = Api.service.fetchMonthJpn(ReqData(month))
        Log.d("TEST", res.raw().toString())

        val monthJpn = res.body()!!.monthJpn
        monthJpn.let {
            Log.d("TEST", it.name + ":" + it.javaClass)
        }
    } catch (e: JsonDataException) {
        Log.e("TEST", e.message!!)
    }
}

// moshiとretrofit
private val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()

private val retrofit = Retrofit.Builder()
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .baseUrl(BASE_URL)
    .build()

1. enumを文字列として受け取る場合(名前で返却の場合)

enumを文字列として受け取る場合は、特に工夫なくそのままEnumとしてデシリアライズしてくれる。

D/TEST: Response{protocol=http/1.1, code=200, message=OK, url=http://10.0.2.2:8000/api/fetch_month_jpn/}
D/TEST: KISARAGI:class jp.co.sample.Wareki

2. enumを数値として受け取る場合(序数で返却の場合)

序数として受け取る場合は、デシリアライズできず例外が発生する。

E/TEST: Expected one of [MUTSUKI, KISARAGI, YAYOI, UDUKI, SATSUKI, MINADUKI, FUMIDUKI, HADUKI, NAGATUKI, KANNADUKI, SHIMOTUKI, SHIWASU, UNKNOWN] but was 1 at path $.month_jpn

アダプターを導入する

こういった場合はアダプターを導入するとうまくいった。

まずはこんなアダプターを作成する。

class WarekiAdapter {
    @FromJson
    fun fromJson(monthJpn: Int): Wareki {
        return Wareki.values().find { it.ordinal == monthJpn } ?: throw IllegalArgumentException()
    }
}

moshiにaddするアダプターには、FromJsonかToJsonかのどちらかが必要。
今回はデシリアライズのみなので、FromJsonのみ定義し、jsonから受け取った値と対応するenum値を
findで探して返却する。見つからない場合は例外を発出するような感じで作成した。

これをmoshiにaddする。

private val moshi = Moshi.Builder()
    .add(WarekiAdapter()) // ここ
    .add(KotlinJsonAdapterFactory())
    .build()

以上。

再度APIをコールすると、うまいことデシリアライズしてくれる。

D/TEST: Response{protocol=http/1.1, code=200, message=OK, url=http://10.0.2.2:8000/api/fetch_month_jpn/}
D/TEST: KISARAGI:class jp.co.sample.Wareki

appendix

ちなみに、上記のアダプターを追加した状態で「1. enumを文字列として受け取る場合」を実施するとエラーになりました

E/TEST: Expected an int but was KISARAGI at path $.month_jpn

どちらでもデシリアライズしてくれるやり方を募集しています。