Kotlinx.serialization: How to convert map into json string

Created on 9 Mar 2020  Â·  25Comments  Â·  Source: Kotlin/kotlinx.serialization

example
val params = mutableMapOf()
params.put("long", 100L)
params.put("int", 10)
params.put("string", "haha")
params.put("map", mutableMapOf("longg" to 10L, "stringg" to "ok"))
LogUtil.d("test", json.stringify(params))

question

Most helpful comment

@gabin8 No it won't. Could you pleas give a use-case for that?

use case:
I want to be able to submit a json body without cluttering up the code.
Expected behavior:

body = mapOf(
                    "login" to email,
                    "password" to password,
                    "tcConsent" to true,
                    "gdprMarketingConsent" to true
                )
            )

Actual behavior:

body = JsonObject(
                mapOf(
                    "login" to JsonPrimitive(email),
                    "password" to JsonPrimitive(password),
                    "tcConsent" to JsonPrimitive(true),
                    "gdprMarketingConsent" to JsonPrimitive(true)
                )
            )

All 25 comments

Use JsonObject instead of Map, see #296

@sandwwraith It would be great to have such functionality with raw map. Can it be submitted as a feature request?

@gabin8 No it won't. Could you pleas give a use-case for that?

sometime we need to provide some scalability. we need to dynamically put data into jsonobject. but the interface dont have a method to do it. At present, I implemented it by converting jsonobject.content to mutablemap, and then I encountered many problems in IOS platform. I don't know how to solve them.i am a android developer, this way can work in android.

You can use JsonObjectBuilder/JsonArrayBuilder for that. See json {} DSL function.

@gabin8 No it won't. Could you pleas give a use-case for that?

use case:
I want to be able to submit a json body without cluttering up the code.
Expected behavior:

body = mapOf(
                    "login" to email,
                    "password" to password,
                    "tcConsent" to true,
                    "gdprMarketingConsent" to true
                )
            )

Actual behavior:

body = JsonObject(
                mapOf(
                    "login" to JsonPrimitive(email),
                    "password" to JsonPrimitive(password),
                    "tcConsent" to JsonPrimitive(true),
                    "gdprMarketingConsent" to JsonPrimitive(true)
                )
            )

One use case I came across is that I'm using Firestore as a DB, which returns values as Maps from their JVM SDK and I can't parse them to custom objects directly because of a limitation in the Properties parser (see https://github.com/Kotlin/kotlinx.serialization/issues/826).

The only solution left for this is to convert the Map to a JSON string and then use the Json parser to convert it to a custom object.

As another use case: Flutter's platform channels uses a HashMap arguments that would be nice to serialize into objects.
Attempting to cast to JsonObject fails.

there is no json available in kotlinx import kotlinx.serialization.json.*

@sreexamus are you talking about kotlinx.serialization.json.Json? What version of the library are you using?

I am evaluating if importing to my project or not.
Map works on Gson and Moshi, but not Kotlinx.Serialization.
It means I have to modify my models from Map to Map if I decide to migrate to Kotlinx.Serialization.
Maybe someday I have to reset to Map when our team decide to remove Kotlinx.Serialization.
I think it's a big cost.

Could you please show an example ow Map<String, Any> with Moshi?

case 1:
value of properties can be anything, like Number, String, array of object or array

data class BridgeEventData(
  @Json(name = "eventName") val eventName: String,
  @Json(name = "properties") val properties: Map<String, Any>
)
val eventJsonString= "json object string"
val eventData = moshi.adapter(BridgeEventData::class.java)
        .fromJson(eventJsonString)



md5-3b0fd103fde9dc443b56a5954b09a38f



    val moshi = Moshi.Builder()
        .build()

    val data = mapOf(
        "id" to 123,
        "name" to "nameOfProduct",
        "price" to 1.23
    )

    val type = Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java)
    val jsonString = moshi.adapter<Map<String, Any>>(type)
        .toJson(data)

When I migrated to Moshi from Gson, I didn't need to modify data.
But I have to do some change in order to migrate to Kotlinx.Serialization

I have also a problem finding the right way to de/serialize a data class with a field of Map<String, Any?>

I hope it's OK to add this example here.

@Serializable
data class Template(val name: String, val fieldCount: Int)

@Serializable
data class Data(
    val templates: Map<String, Any?>
)

val example = Data(
    templates = mapOf(
        "outer" to Template("X", 5),
        "withInner" to mapOf("inner" to Template("Y", 3))
    )
)

(this is a simple example. Our Template is actually polymorphic)

Any news for this issue?

Hi @sandwwraith

We have an AWS lambda that passes its input as a LinkedHashMap where the key is a string and the
value is either a String or another LinkedHashMap.

E.g.

public APIGatewayProxyResponseEvent handleRequest(LinkedHashMap request, Context context)

Currently, we use gson to create a JsonElement so we can parse the input request and then parse it as a String to convert it to an object.

JsonElement requestJson = gson.toJsonTree(request, LinkedHashMap.class);
return gson.fromJson(requestJson, DelegationRequest.class);

I couldn't find anything similar in Kotlin Serialization, is it possible to achieve the same thing?

@markchristopherng Simply convert Map to JsonElement recursively — JsonObject has a constructor that accepts Map<String, JsonElement> argument

@sandwwraith Thanks for that, it would be good if there was some type of convenience method because if developers get used to using this on Gson or whatever other framework they use for JSON parsing then they would expect it for new libraries like this.

It would be good to provide a migration guide from

Gson -> Kotlin Serialization
Jackson -> Kotlin Serialization

Could help with adoption and also undercover whether Kotlin Serialization is harder or easier to use than these existing frameworks. I know that Kotlin Serialization provides more than just JSON parsing but having good documentation & migration guides is key to adoption.

I'm used to using like this.

fun Any?.toJsonElement(): JsonElement {
    return when (this) {
        is Number -> JsonPrimitive(this)
        is Boolean -> JsonPrimitive(this)
        is String -> JsonPrimitive(this)
        is Array<*> -> this.toJsonArray()
        is List<*> -> this.toJsonArray()
        is Map<*, *> -> this.toJsonObject()
        is JsonElement -> this
        else -> JsonNull
    }
}

fun Array<*>.toJsonArray(): JsonArray {
    val array = mutableListOf<JsonElement>()
    this.forEach { array.add(it.toJsonElement()) }
    return JsonArray(array)
}

fun List<*>.toJsonArray(): JsonArray {
    val array = mutableListOf<JsonElement>()
    this.forEach { array.add(it.toJsonElement()) }
    return JsonArray(array)
}

fun Map<*, *>.toJsonObject(): JsonObject {
    val map = mutableMapOf<String, JsonElement>()
    this.forEach {
        if (it.key is String) {
            map[it.key as String] = it.value.toJsonElement()
        }
    }
    return JsonObject(map)
}

+1 for this -- its really common for libraries to treat JSON data as a Map<String, Any>. Libraries like Jackson can convert between JSON (either the serialized String or an object representation) and maps easily.

Even org.json.JSONObject can do this: https://developer.android.com/reference/org/json/JSONObject

I just use this then deserialize JSONObject.toString() with kotlinx.

If anyone is looking for generic Map<String, Any> serialization based on some preliminary testing this seems to work for me

@Serializable
data class Generic<T>(
    val data: T? = null,
    val extensions: Map<String, @Serializable(with = AnySerializer::class) Any>? = null
)

object AnySerializer : KSerializer<Any> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Any")

    override fun serialize(encoder: Encoder, value: Any) {
        val jsonEncoder = encoder as JsonEncoder
        val jsonElement = serializeAny(value)
        jsonEncoder.encodeJsonElement(jsonElement)
    }

    private fun serializeAny(value: Any?): JsonElement = when (value) {
        is Map<*, *> -> {
            val mapContents = value.entries.associate { mapEntry ->
                mapEntry.key.toString() to serializeAny(mapEntry.value)
            }
            JsonObject(mapContents)
        }
        is List<*> -> {
            val arrayContents = value.map { listEntry -> serializeAny(listEntry) }
            JsonArray(arrayContents)
        }
        is Number -> JsonPrimitive(value)
        is Boolean -> JsonPrimitive(value)
        else -> JsonPrimitive(value.toString())
    }

    override fun deserialize(decoder: Decoder): Any {
        val jsonDecoder = decoder as JsonDecoder
        val element = jsonDecoder.decodeJsonElement()

        return deserializeJsonElement(element)
    }

    private fun deserializeJsonElement(element: JsonElement): Any = when (element) {
        is JsonObject -> {
            element.mapValues { deserializeJsonElement(it.value) }
        }
        is JsonArray -> {
            element.map { deserializeJsonElement(it) }
        }
        is JsonPrimitive -> element.toString()
    }
}

*obviously it will only work with primitives and maps/arrays of primitives - if you attempt to serialize/deserialize complex objects it won't work. Guess you could use reflections to iterate over all fields but wouldn't that be overkill?

I ended up using reflections.... updated serializeAny method below

private fun serializeAny(value: Any?): JsonElement = when (value) {
    null -> JsonNull
    is Map<*, *> -> {
        val mapContents = value.entries.associate { mapEntry ->
            mapEntry.key.toString() to serializeAny(mapEntry.value)
        }
        JsonObject(mapContents)
    }
    is List<*> -> {
        val arrayContents = value.map { listEntry -> serializeAny(listEntry) }
        JsonArray(arrayContents)
    }
    is Number -> JsonPrimitive(value)
    is Boolean -> JsonPrimitive(value)
    is String -> JsonPrimitive(value)
    else -> {
        val contents = value::class.memberProperties.associate { property ->
            property.name to serializeAny(property.getter.call(value))
        }
        JsonObject(contents)
    }
}

Taking @WontakKim's solution a step further, this appears to work nicely (although could come with caveats I don't yet understand)

fun Any?.toJsonElement(): JsonElement = when (this) {
    is Number -> JsonPrimitive(this)
    is Boolean -> JsonPrimitive(this)
    is String -> JsonPrimitive(this)
    is Array<*> -> this.toJsonArray()
    is List<*> -> this.toJsonArray()
    is Map<*, *> -> this.toJsonObject()
    is JsonElement -> this
    else -> JsonNull
}

fun Array<*>.toJsonArray() = JsonArray(map { it.toJsonElement() })
fun Iterable<*>.toJsonArray() = JsonArray(map { it.toJsonElement() })
fun Map<*, *>.toJsonObject() = JsonObject(mapKeys { it.key.toString() }.mapValues { it.value.toJsonElement() })

fun Json.encodeToString(vararg pairs: Pair<*, *>) = encodeToString(pairs.toMap().toJsonElement())

usage

val json = Json {}
val str = json.encodeToString(
    "key1" to "string value",
    "key2" to 123,
    "key3" to true,
)
println(str) // {"key1":"string value","key2":123,"key3":true}

I'm using the graphql-java library which returns a Map<String, Object> (i.e., Map<String, Any>), and I'm unable to figure out how to properly convert this into a JsonObject or JsonElement which means it's impossible for me to use kotlinx.serialization. I only realized this after already spending ~2 days migrating nearly a thousand lines of serialization code, and now I've rolled back to using jackson instead. I thought it'd be trivial to serialize a Map<String, Any> because gson and jackson do it out of the box but apparently not. I suggest one of the following be done:

  • I understand that there was an explicit (and useful) design decision to not use reflection. If kotlinx.serialization is unable to serialize Map<String, Any>, etc. due to this design decision, then this must be clearly stated in the README to prevent developers from running into such an issue.
  • If it's possible to properly convert a Map<String, Any> to either a JsonObject or JsonElement such as by using one of the many code snippets provided above, then this should be included in the library because it's a basic use case for many developers.
Was this page helpful?
0 / 5 - 0 ratings