10

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.

8
  • Is it possible that you get duplicated key in json? Commented Apr 14, 2019 at 12:58
  • All keys are unique, values could be duplicated Commented Apr 14, 2019 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 Commented Apr 14, 2019 at 13:07
  • No, I am writing the client. The server is returning that and I don't have access to it. Commented Apr 14, 2019 at 13:08
  • github.com/Kotlin/kotlinx.serialization/blob/master/docs/… Commented Apr 14, 2019 at 13:10

2 Answers 2

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}") }
}
Sign up to request clarification or add additional context in comments.

2 Comments

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!
Hey man, how'd you do the SerialClassDescImpl("JsonMap")? Do I have do define that too?
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>
)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.