9

I am writing a Kotlin multiplatform project (JVM/JS) and I am trying to parse a HTTP Json array response into a Map using Kotlinx.serialization

The JSON is something like this:

[{"someKey": "someValue"}, {"otherKey": "otherValue"}, {"anotherKey": "randomText"}]

So far, I am able to get that JSON as String, but I can't find any documentation to help me build a Map or another kind of object. All of it says how to serialize static objects.

I can't use @SerialName because the key is not fixed.

When I try to return a Map<String, String>, I get this error:

Can't locate argument-less serializer for class kotlin.collections.Map. For generic classes, such as lists, please provide serializer explicitly.

At the end, I would like to get either a Map<String, String> or a List<MyObject> where my object could be MyObject(val id: String, val value: String)

Is there a way to do that? Otherwise I am thinking in just writing a String reader to be able to parse my data.

Rulo Mejía
  • 158
  • 1
  • 5
  • Is it possible that you get duplicated key in json? – Mosius Apr 14 '19 at 12:58
  • All keys are unique, values could be duplicated – Rulo Mejía Apr 14 '19 at 13:04
  • 1
    if there is any chance to refactor your json try to put all your objects inside one object like `{"someKey": "someValue", "otherKey": "otherValue"}` it is a better data structure to use – Mosius Apr 14 '19 at 13:07
  • No, I am writing the client. The server is returning that and I don't have access to it. – Rulo Mejía Apr 14 '19 at 13:08
  • https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/custom_serializers.md – royB Apr 14 '19 at 13:10
  • Why not pars it into `List>` as an internal step in your client, and from there make it one of your desired types. – Laurence Apr 14 '19 at 13:18
  • @royB Thanks, I am reading through the article, and is still hard to write my own serializer. I will try both ways, to make a Map and to make my own object and I would take what I could do first. – Rulo Mejía Apr 14 '19 at 13:24
  • @Laurence, that would be great, but I don't know how to parse it as a List. I get the same `Can't locate argument-less serializer for` but now for List. – Rulo Mejía Apr 14 '19 at 13:25

2 Answers2

10

You can implement you own simple DeserializationStrategy like this:

object JsonArrayToStringMapDeserializer : DeserializationStrategy<Map<String, String>> {

    override val descriptor = SerialClassDescImpl("JsonMap")

    override fun deserialize(decoder: Decoder): Map<String, String> {

        val input = decoder as? JsonInput ?: throw SerializationException("Expected Json Input")
        val array = input.decodeJson() as? JsonArray ?: throw SerializationException("Expected JsonArray")

        return array.map {
            it as JsonObject
            val firstKey = it.keys.first()
            firstKey to it[firstKey]!!.content
        }.toMap()


    }

    override fun patch(decoder: Decoder, old: Map<String, String>): Map<String, String> =
        throw UpdateNotSupportedException("Update not supported")

}


fun main() {
    val map = Json.parse(JsonArrayToStringMapDeserializer, data)
    map.forEach { println("${it.key} - ${it.value}") }
}
Alexander Egger
  • 5,132
  • 1
  • 28
  • 42
  • Works perfectly!! The workaround I was working on was to use an **expect class** parser and write both implementations in jvm with klaxon (which I finished) and in js with JSON.parse, but this covers both cases. Thanks! – Rulo Mejía Apr 14 '19 at 18:06
  • Hey man, how'd you do the `SerialClassDescImpl("JsonMap")`? Do I have do define that too? – Richard Domingo Sep 04 '21 at 15:28
0

As the answer by @alexander-egger looks a bit outdated, here is a modern one:

object ListAsMapDeserializer: KSerializer<Map<String, String>> {

    private val mapSerializer = ListSerializer(MapEntrySerializer(String.serializer(), String.serializer()))

    override val descriptor: SerialDescriptor = mapSerializer.descriptor

    override fun deserialize(decoder: Decoder): Map<String, String> {
        return mapSerializer.deserialize(decoder).associate { it.toPair() }
    }

    override fun serialize(encoder: Encoder, value: Map<String, String>) {
        mapSerializer.serialize(encoder, value.entries.toList())
    }
}

and tests for it :

@Test
fun listAsMap() {
    val jsonElement = json.parseToJsonElement("{ \"map\": [ {\"key1\":\"value1\"}, {\"key2\":\"value2\"} ] }")
    val testWithMap = json.decodeFromJsonElement<TestWithMap>(jsonElement)
    assertEquals(mapOf("key1" to "value1", "key2" to "value2"), testWithMap.map)
}

@Test
fun mapAsList() {
    val jsonElement = json.parseToJsonElement("{ \"map\": [ {\"key1\":\"value1\"}, {\"key2\":\"value2\"} ] }")
    val testWithMap = TestWithMap(mapOf("key1" to "value1", "key2" to "value2"))
    val serialized = json.encodeToJsonElement(TestWithMap.serializer(), testWithMap)
    assertEquals(jsonElement, serialized)
}

@Serializable
data class TestWithMap(
    @Serializable(with = ListAsMapDeserializer::class)
    val map: Map<String, String>
)
akd005
  • 540
  • 4
  • 13