skip to Main Content

I am currently implementing an API client with Ktor. The API I am requesting does not return a consistent JSON format.

for Example:

sometimes the JSON looks like this:

{
    "description": {
        "lang": "en",
        "value": "an English description..."
    },
    ...
}

and sometimes like this:

{
    "description": [
        {
            "lang": "en",
            "value": "an English description..."
        },
        {
            "lang": "fr",
            "value": "a French description..."
        }
    ],
    ...
}

Now my Question:
How can I implement a Custom Kotlinx Deserializer to Decode an Object of T or a List<T> to a List<T>

My classes look like this:

@Serializable
class ResourceResponse(
  @SerialName("description")
  val descriptions: List<Description>
) {
  @Serializable
  data class Description(
    @SerialName("value")
    val value: String,

    @SerialName("lang")
    val language: String,
  )
}

I want that a Json with only one Description-Object will be deserialized to a List with one Object and not specifically for the description, but in general for classes.

I’ve found nothing really helpful in the Web.

4

Answers


  1. Chosen as BEST ANSWER

    After my research, I have now come up with a solution. For this you need a wrapper class. (here GenericResponse). I hope I can help others who have the same problem.

    This is the Wrapper-Class

    @Serializable(with = ListOrObjectSerializer::class)
    class GenericResponse<T>(
      val data: List<T> = emptyList()
    ) {
    
      private var _isNothing : Boolean = false
    
      val isNothing: Boolean
        get() {
          return this._isNothing
        }
    
      companion object {
        fun <T> nothing(): GenericResponse<T> {
          val o = GenericResponse(emptyList<T>())
          o._isNothing = true
          return o
        }
      }
    }
    

    And the Serializer looks like:

    import kotlinx.serialization.KSerializer
    import kotlinx.serialization.builtins.ListSerializer
    import kotlinx.serialization.descriptors.SerialDescriptor
    import kotlinx.serialization.encoding.Decoder
    import kotlinx.serialization.encoding.Encoder
    import kotlinx.serialization.json.*
    
    class ListOrObjectSerializer<T : Any>(private val tSerializer: KSerializer<T>): KSerializer<GenericResponse<T>> {
    
      override val descriptor: SerialDescriptor
        get() = tSerializer.descriptor
    
      override fun deserialize(decoder: Decoder): GenericResponse<T> {
        val input = decoder as JsonDecoder
        val jsonObj = input.decodeJsonElement()
    
        return when(jsonObj) {
          is JsonObject ->  GenericResponse(listOf(Json.decodeFromJsonElement(tSerializer, jsonObj)))
          is JsonArray -> GenericResponse(Json.decodeFromJsonElement(ListSerializer(tSerializer), jsonObj))
          else -> return GenericResponse.nothing()
        }
      }
    
      override fun serialize(encoder: Encoder, value: GenericResponse<T>) {
        throw IllegalAccessError("serialize not supported")
      }
    }
    

    My Data-Class look now like:

    import kotlinx.serialization.SerialName
    import kotlinx.serialization.Serializable
    
    @Serializable
    class ResourceResponse(
      @SerialName("description")
      val descriptions: GenericResponse<Description>? = null,
    ) {
      @Serializable
      data class Description(
        @SerialName("value")
        val value: String? = null,
    
        @SerialName("lang")
        val language: String? = null,
      )
    }
    

  2. data class ResourceResponse(
        @SerializedName("description") val descriptions: List<Description>,
    )
    
    data class Description(
        @SerializedName("value") val value: String,
        @SerializedName("lang") val language: String,
    )

    it should be like that

    Login or Signup to reply.
  3. One solution is to first deserialize it to JsonElement, introspect and then decide how to deserialize it further into ResourceResponse:

    fun decode(s: String): ResourceResponse {
        val json = Json.parseToJsonElement(s).jsonObject
        return when (val desc = json["description"]) {
            is JsonArray -> Json.decodeFromJsonElement(json)
            is JsonObject -> {
                val json2 = json.toMutableMap()
                json2["description"] = JsonArray(listOf(desc))
                Json.decodeFromJsonElement(JsonObject(json2))
            }
            else -> throw IllegalArgumentException("Invalid value for "description": $desc")
        }
    }
    

    This solution is definitely not ideal. It may be potentially less performant as we need to deserialize the whole tree into the tree of JsonElement objects only to transform it to the final types (although, maybe the library does this internally anyway). It works only for json and it is tricky to use this solution if ResourceResponse is somewhere deep into the data structure.

    Login or Signup to reply.
  4. You can use a JsonContentPolymorphicSerializer to choose a deserializer based on the form of the JSON.

    This one should work:

    @Suppress("UNCHECKED_CAST")
    class DescriptionsSerializer : JsonContentPolymorphicSerializer<List<ResourceResponse.Description>>(
        List::class as KClass<List<ResourceResponse.Description>>
    ) {
        // Here we check the form of the JSON we are decoding, and choose
        // the serializer accordingly
        override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out List<ResourceResponse.Description>> {
            return if (element is JsonArray)
                ListSerializer(ResourceResponse.Description.serializer())
            else
                SingleDescriptionAsList()
        }
    
        class SingleDescriptionAsList : KSerializer<List<ResourceResponse.Description>> {
            override val descriptor: SerialDescriptor
                get() = ResourceResponse.Description.serializer().descriptor
    
            override fun deserialize(decoder: Decoder): List<ResourceResponse.Description> {
                return listOf(ResourceResponse.Description.serializer().deserialize(decoder))
            }
    
            override fun serialize(encoder: Encoder, value: List<ResourceResponse.Description>) {
                throw Exception("Not in use")
            }
        }
    }
    

    You must also amend your original class to tell it to use this serializer:

    @Serializable
    class ResourceResponse(
        @SerialName("description")
        @Serializable(with = DescriptionsSerializer::class) val descriptions: List<Description>
    ) {
        @Serializable
        data class Description(
            @SerialName("value")
            val value: String,
    
            @SerialName("lang")
            val language: String,
        )
    }
    

    Then you will be able to decode JSON objects with the single key "descriptions" using the ResourceResponse serializer.

    For avoidance of doubt, if there are other keys in the JSON (it’s not entirely clear from the question) then those should also be written into ResourceResponse definition.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search