trfalgarlaw
trfalgarlaw

Reputation: 73

Unable to create converter for class when using sealed class or an interface with Moshi

I am trying to parse a json data from a server.It has dynamic keys so I am trying to have like a parent class that have the shared keys and child class for each specific node. I wrote a kotlin code using retrofit and Moshi but it's not working. I tried with a sealed class and interface without success. Actually I would prefer that works with sealed class but I don't know what I am doing wrong

interface MyApi {

    @GET("/...")
    fun fetchMyFeed(): Call<MyResponse>

}

data class MyResponse(
    val data: List<ParentResponse>
)
interface ParentResponse{
    val name: String
}

data class Child1Response(
    val age: String,
    val kids: List<KidsResponse>,
    val cars: List<CarsResponse>
)

data class Child2Response(
    val job: String,
    val address: List<AddressResponse>
)


fun fetchAllFeed(): List<Any>? =
        try {
            val response = api.fetchMyFeed().execute()
            if (response.isSuccessful) {
                Log.d("check",${response.body()?.data?})
                null
            } else null
        } catch (e: IOException) {
            null
        } catch (e: RuntimeException) {
            null
        }```

and the json file is : 

{
  "data": [
    {
      "name": "string",
      "job": "string",
      "address": [
        {
          "avenue": "string",
          "imageUrl": "string",
          "description": "string"
        }
      ]
    },
    {
      "name": "string",
      "age": "string",
      "kids": {
        "count": "string",
        "working": "string"
      },
      "cars": [
        {
          "brand": "string",
          "age": "string",
          "imageUrl": "string"
        }
      ]
    }
  ]
}

Unable to create converter for class

Upvotes: 4

Views: 4015

Answers (1)

Jegan Babu
Jegan Babu

Reputation: 1396

You can make use of JsonAdapter from moshi to parse different JSON Models if you can differentiate them by foreseeing some value in the json.

for example, consider json response having two schemas,

{
  "root": {
      "subroot": {
         "prop" : "hello",
         "type" : "String"
       }
   }
}

(or)

{
  "root": {
      "subroot": {
         "prop" : 100,
         "type" : "Integer"
       }
   }
}

Here, subroot has different schemas (one containing string property and another containg a integer property) which can be identified by "type"

You can create a parent sealed class with common keys and derive few child classes with varying keys. Write a adapter to select the type of class to be used while json serialization and add that adapter to moshi builder.

Model classes:

class Response {
    @Json(name = "root")
    val root: Root? = null
}

class Root {
    @Json(name = "subroot")
    val subroot: HybridModel? = null
}

sealed class HybridModel {
    @Json(name = "type")
    val type: String? = null

    class StringModel : HybridModel() {
        @Json(name = "prop")
        val prop: String? = null
    }

    class IntegerModel : HybridModel() {
        @Json(name = "prop")
        val prop: Int? = null
    }
}

Few extension methods to JsonReader,

inline fun JsonReader.readObject(process: () -> Unit) {
    beginObject()
    while (hasNext()) {
        process()
    }
    endObject()
}

fun JsonReader.skipNameAndValue() {
    skipName()
    skipValue()
}

HybridAdapter to select type of class for "subroot" key

class HybridAdapter : JsonAdapter<HybridModel>() {
    @FromJson
    override fun fromJson(reader: JsonReader): HybridModel {
        var type: String = ""

        // copy reader and  foresee type
        val copy = reader.peekJson()
        copy.readObject {
            when (copy.selectName(JsonReader.Options.of("type"))) {
                0 -> {
                    type = copy.nextString()
                }
                else -> copy.skipNameAndValue()
            }
        }

        //handle exception if type cannot be identified
        if (type.isEmpty()) throw JsonDataException("missing type")

        // build model based on type
        val moshi = Moshi.Builder().build()
        return if (type == "String")
            moshi.adapter(HybridModel.StringModel::class.java).fromJson(reader)!!
        else
            moshi.adapter(HybridModel.IntegerModel::class.java).fromJson(reader)!!
    }

    @ToJson
    override fun toJson(p0: JsonWriter, p1: HybridModel?) {
        // serialization logic
    }
}

Finally build Moshi with the HybridAdapter to serialize HybridModel,

fun printProp(response: Response?) {
    val subroot = response?.root?.subroot
    when (subroot) {
        is HybridModel.StringModel -> println("string model: ${subroot.prop}")
        is HybridModel.IntegerModel -> println("Integer model: ${subroot.prop}")
    }
}

fun main() {
    val jsonWithStringSubroot =
    """
    {
        "root": {
            "subroot": {
                "prop" : "hello",
                 "type" : "String"
            }
        }
    }
    """
    val jsonWithIntegerSubroot =
    """
    {
        "root": {
            "subroot": {
                "prop" : 1,
                 "type" : "Integer"
            }
        }
    }
    """

    val moshi = Moshi.Builder().add(HybridAdapter()).build()

    val response1 = moshi.adapter(Response::class.java).fromJson(jsonWithStringSubroot)
    printProp(response1)  // contains HybridModel.StringModel

    val response2 = moshi.adapter(Response::class.java).fromJson(jsonWithIntegerSubroot)
    printProp(response2) // contains HybridModel.IntegerModel
}

Upvotes: 5

Related Questions