Search code examples
androidjsonkotlinserializationmoshi

Moshi: Problems serializing List<MyClass>


I'm migrating Gson to Moshi, and I'm having problems serializing List<object>. I have no problem with "plain" objects, but with Lists.

I have a class ImageDTO (including a Moshi adapter) like this:

class ImageDTO (
    val idImage: String = "",
    val idQuestion: String = "",
    val image: String = "",
    val timestamp: Int = 0,
    var saveMode: String = ""
) {
    @JsonClass(generateAdapter = true)
    class ImageDTOIntermediate(
        @Json(name = "idImage")
        val idImage: String,
        @Json(name = "idQuestion")
        val idQuestion: String,
        @Json(name = "image")
        val image: String,
        @Json(name = "timestamp")
        val timestamp: Int,
        @Json(name = "saveMode")
        var saveMode: String = ""
    )

    companion object {
        val JSON_ADAPTER: Any = object : Any() {
            @ToJson
            private fun toJson(imageDTO: ImageDTO): String {

                val idImage = imageDTO.idImage
                val idQuestion = imageDTO.idQuestion
                val image = imageDTO.image
                val timestamp = imageDTO.timestamp
                val saveMode = imageDTO.saveMode

                val imageDTOIntermediate = ImageDTOIntermediate(idImage, idQuestion, image, timestamp, saveMode)

                return imageDTOIntermediate.serialize()
            }

            @FromJson
            private fun fromJson(imageDTOIntermediate: ImageDTOIntermediate): ImageDTO {

                val idImage = imageDTOIntermediate.idImage
                val idQuestion = imageDTOIntermediate.idQuestion
                val image = imageDTOIntermediate.image
                val timestamp = imageDTOIntermediate.timestamp
                val saveMode = imageDTOIntermediate.saveMode

                return ImageDTO(idImage, idQuestion, image, timestamp, saveMode)
            }
        }
    }
}

And when serializing I expect to have a Json like this next:

[
   {
      "idImage":"f3d4befc-4569-c0c5-19b1-2865fcab5d33",
      "idQuestion":"65d5254b-cb6d-a099-9f6d-71d93dcdcadb",
      "image":"65d5254b-cb6d-a099-9f6d-71d93dcdcadb_q5_1.png",
      "saveMode":"",
      "timestamp":1498856479
   },
   {
      "idImage":"66eb6535-f4b2-a130-6e90-cf53ce95bb63",
      "idQuestion":"d23f87c0-cbc5-84b8-34cd-f5be4faced21",
      "image":"d23f87c0-cbc5-84b8-34cd-f5be4faced21_i4_1.png",
      "saveMode":"",
      "timestamp":1538922258
   },
   {
      "idImage":"016bba85-a023-8fb0-5aa4-71a0665dab94",
      "idQuestion":"ad8e3eec-bd5a-aefa-fedf-28d51d82ab49",
      "image":"ad8e3eec-bd5a-aefa-fedf-28d51d82ab49_i5_1.png",
      "saveMode":"",
      "timestamp":1626640944
   }
]

Using Gson this way:

private var images: List<ImageDTO>
...
Gson().toJson(images)

I am successfully getting the expected Json, but using moshi with the adapter given above, I get a Json string apparently correct, but with escaped double quotes (\"), what makes sense to me given the serialize in ToJson method in the adapter. Every single ImageDTO object is serialized correctly, but when building the list the final string has all inner double quotes escaped, and I don't know how to handle this.

This next is my Moshi/Json extension class:

object TMJson {

    val moshi: Moshi = Moshi.Builder()
        .add(User.JSON_ADAPTER)
        .add(ImageDTO.JSON_ADAPTER)
        .build()

    inline fun <reified T> T.serialize(): String {
        val adapter: JsonAdapter<T> = moshi.adapter(T::class.java)
        return adapter.toJson(this)
    }

    inline fun <reified T> String.deserialize(): T? {
        val adapter: JsonAdapter<T> = moshi.adapter(T::class.java)
        return adapter.fromJson(this)
    }

    inline fun <reified T> String.serialize(): MutableList<T>? {
        return moshi.serialize(this)
    }

    inline fun <reified T> Moshi.serialize(jsonString: String): MutableList<T>? {
        return adapter<MutableList<T>>(Types.newParameterizedType(List::class.java, T::class.java)).fromJson(jsonString)
    }
}

I guess my Moshi adapter can be simplified, but not sure exactly how, and this is not my main problem, but the List that is being serialized like this (I'm not putting the values here for simplicity:

["{\"idImage\":\"\",\"idQuestion\":\"\",\"image\":\"\",\"timestamp\":0,\"saveMode\":\"\"}","{\"idImage\":\"\",\"idQuestion\":\"\",\"image\":\"\",\"timestamp\":0,\"saveMode\":\"\"}","{\"idImage\":\"\",\"idQuestion\":\"\",\"image\":\"\",\"timestamp\":0,\"saveMode\":\"\"}","{\"idImage\":\"\",\"idQuestion\":\"\",...]

I don't want to use KotlinJsonAdapterFactory(), but a custom adapter as it's faster.

In my build.gradle:

implementation('com.squareup.moshi:moshi-kotlin:1.15.0')
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")

How can I serialize my List<ImageDTO> correctly?

Edit 1: This is how I call the json/moshi extensions.

private var images: List<ImageDTO>?
...
override fun call(): String {
    images = getImagesFromTests(testsDTO)
    val images1 = Gson().toJson(images) //=> Works
    val images2 = images!!.serialize1() //=> Does not work
    val tests = updateLocalDatabaseTests(testsDTO)
    return "{\"tests\": [$tests], \"images\": [$images1]}"
}

I have to add that "call" method is a background task, and images (the List of ImageDTO) already has values when "call" method is called.

Edit 2: Extra info about the project.

I don't think it's relevant, but just in case I will add that my app is a multi-module app. The class "ImageDTO" is located into a MyApp.DTO module, while TMJson extensions are in MyApp.Common module.

Due to circular dependencies, I cannot reference "Common" in "DTO", so I ended up by cloning the "TMJson" extensions class with serialize methods.

What confuses me is the need to use an intermediate object to be able to serialize when there is no data modification at all, but this is from an example I've found on the net and it's the only way I know to do that.


Solution

  • The problem is in the adapter object's @ToJson. Currently, your @ToJson is returning a String type. Moshi sees this string as a string that still needs to be encoded to JSON, but you're actually returning an encoded JSON string. That's causing the double encoding.

    All you need to do is return your ImageDTOIntermediate, mirroring your @FromJson. Let Moshi get the adapter for the ImageDTOIntermediate for you.

    @ToJson
    private fun toJson(imageDTO: ImageDTO): ImageDTOIntermediate {
      val idImage = imageDTO.idImage
      val idQuestion = imageDTO.idQuestion
      val image = imageDTO.image
      val timestamp = imageDTO.timestamp
      val saveMode = imageDTO.saveMode
      return ImageDTOIntermediate(idImage, idQuestion, image, timestamp, saveMode)
    }