0

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.

  • I think you need to use a custom serializer: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#custom-serializers – broot Feb 15 '23 at 08:38
  • Yes, I have already written that I need a custom serializer. But what does a generic serializer look like in general for objects or lists of objects? – Moritz Großmann Feb 15 '23 at 09:18
  • Ahh, sorry for that. Now I see the problem here is probably that the serializer API in kotlinx.serialization doesn't really like the idea of varying json format. Even while writing our fully custom deserializer, we still have to provide a descriptor of the format and it is fixed :-/ Is this the problem? Maybe initially deserialize to `JsonElement`, verify the type and then deserialize `JsonElement` to `ResourceResponse` accordingly? I don't know if/how does it affect the performance though. – broot Feb 15 '23 at 09:37

4 Answers4

1

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.

Simon Jacobs
  • 1,147
  • 7
  • 7
0

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

Huy Tran
  • 1
  • 2
0

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.

broot
  • 21,588
  • 3
  • 30
  • 35
0

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,
  )
}