Android
GSON
Retrofit

Retrofit 2 + Gson で複数種別のオブジェクトの List をパースしたい

いろいろなアプリを作っていると、一度はフィードニュースやチャットの機能を実装することがあるかと思います。
そんな時に、例えばチャット機能だと、テキストのメッセージだったり、画像のメッセージだったりと、複数の種別のメッセージを扱うこととなり、List を返す API のレスポンスが以下のようなものだったりします。

messsage.json
{
  "messages": [
    {
      "type": "TEXT",
      "message": {
        "text": "Hello!"
      }
    },
    {
      "type": "IMAGE",
      "message": {
        "imageUrl": "https://..."
      }
    }
  ]
} 

こういうレスポンスを Retrofit 2 + Gson でパースするには?という記事になります。

実装

上記の例の json をサンプルに、モデルを定義します。

Message.kt
sealed class Message

class TextMessage(
        val text: String
): Message()

class ImageMessage(
        val imageUrl: String
): Message()

enum class MessageType {
    TEXT, IMAGE
}

次に、API のインタフェースを定義。
Message の List を受け取ります。

MessageApi.kt
interface MessagesApi {

    @GET("messages")
    fun getMessages(): Single<List<Message>>
}

ここで、Retrofit から返された Message が、各種別毎のインスタンスとなるよう、Gson の Deserializer を定義します。
context.deserialize<>() に流していくことで、各クラス毎のパースは Gson にお任せできます。

MessageDeserializer.kt
class MessageDeserializer : JsonDeserializer<Message> {

    override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Message {

        val type = json.asJsonObject.get("type").asString
        val message = json.asJsonObject.get("message")

        return when (MessageType.valueOf(type)) {
            MessageType.TEXT -> {
                context.deserialize<TextMessage>(message, TextMessage::class.java)
            }
            MessageType.IMAGE -> {
                context.deserialize<ImageMessage>(message, ImageMessage::class.java)
            }
        }
    }
}

最後に、Gson に Deserializer を登録して完了です。

GsonBuilder()
    .registerTypeAdapter(Message::class.java, MessageDeserializer())
    .create()