Parse a JSON array into Map<String, String> using Kotlinx.serialization
Asked Answered
B

2

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.

Bake answered 14/4, 2019 at 12:50 Comment(8)
Is it possible that you get duplicated key in json?Synergistic
All keys are unique, values could be duplicatedSitka
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 useSynergistic
No, I am writing the client. The server is returning that and I don't have access to it.Sitka
github.com/Kotlin/kotlinx.serialization/blob/master/docs/…Khanate
Why not pars it into List<Map<String,String>> as an internal step in your client, and from there make it one of your desired types.Cottar
@Khanate 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.Sitka
@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.Sitka
J
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}") }
}
Juridical answered 14/4, 2019 at 17:29 Comment(2)
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!Sitka
Hey man, how'd you do the SerialClassDescImpl("JsonMap")? Do I have do define that too?Loathing
A
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>
)
Almagest answered 30/3, 2023 at 11:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.