Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.42k stars 620 forks source link

How to convert map into json string #746

Closed roths closed 2 years ago

roths commented 4 years ago

example val params = mutableMapOf<String, Any>() 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))

sandwwraith commented 4 years ago

Use JsonObject instead of Map, see #296

gabin8 commented 4 years ago

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

altavir commented 4 years ago

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

roths commented 4 years ago

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.

sandwwraith commented 4 years ago

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

gabin8 commented 4 years ago

@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)
                )
            )
francos commented 4 years ago

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.

cpboyd commented 4 years ago

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.

sreexamus commented 4 years ago

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

francos commented 4 years ago

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

aids61517 commented 4 years ago

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

qwwdfsad commented 4 years ago

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

aids61517 commented 4 years ago

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)

case 2: I need to convert a data of product to json string

    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

HelgaSospi commented 4 years ago

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)

yusufceylan commented 4 years ago

Any news for this issue?

markchristopherng commented 3 years ago

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?

sandwwraith commented 3 years ago

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

markchristopherng commented 3 years ago

@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.

WontakKim commented 3 years ago

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)
}
rocketraman commented 3 years ago

+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.

VincentJoshuaET commented 3 years ago

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.

dariuszkuc commented 3 years ago

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?

dariuszkuc commented 3 years ago

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)
    }
}
neumannk commented 3 years ago

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}
neelkamath commented 3 years ago

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:

teble commented 3 years ago

After two days of research, combine @neumannk 's code, I used the following scheme to complete the serialization of any class, although it uses Kotlin reflection. Because I hope he can complete the serialization of Map, List and some Data classes

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.*
import kotlinx.serialization.serializer
import kotlin.reflect.full.createType

fun Any?.toJsonElement(): JsonElement = when (this) {
    null -> JsonNull
    is JsonElement -> this
    is Number -> JsonPrimitive(this)
    is Boolean -> JsonPrimitive(this)
    is String -> JsonPrimitive(this)
    is Array<*> -> JsonArray(map { it.toJsonElement() })
    is List<*> -> JsonArray(map { it.toJsonElement() })
    is Map<*, *> -> JsonObject(map { it.key.toString() to it.value.toJsonElement() }.toMap())
    else -> Json.encodeToJsonElement(serializer(this::class.createType()), this)
}

fun Any?.toJsonString(): String = Json.encodeToString(this.toJsonElement())
nkbai commented 3 years ago
class.memberProperties
 var Results:MutableMap<String,@Serializable(with = AnySerializer::class)Any> =LinkedHashMap()
    Results["extensions"]=mapOf(
        "a" to 3,
        "b" to 7,
        "c" to listOf<Int>(1,2,3),
        "d" to mapOf(
            "e" to 12,
            "f" to listOf<String>("")
        )
    )
    val s2=Json.encodeToString(Results)
    println(s2)

it reports exception: Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found. Mark the class as @Serializable or provide the serializer explicitly.

emartynov commented 2 years ago

I'm a bit puzzled - I got a similar exception

 Fatal Exception: java.lang.ClassCastException
kotlin.reflect.jvm.internal.KTypeImpl cannot be cast to kotlin.jvm.internal.TypeReference

Where I try to save MutableMap<String, MutableList<Int>>. I wrote a unit test for it and I don't see this exception.

The test runs on JVM the exception happens in the Android Runtime.

Does anyone have an explanation?

sandwwraith commented 2 years ago

@emartynov Then it is a separate issue, you can create new ticket with full stacktrace & reproducer

emartynov commented 2 years ago

@sandwwraith how do you know that it is a separate issue? Can you light a bit of detail what is the cause?

Actually, I'm wrong I don't see exception that I mentioned in this ticket. Also already forgot how did I land here. Please ignore 👆🏼.

unoexperto commented 1 year ago

@roths It's fairly difficult to get customer friendly improvement or advice from JetBrains team. You could use moshi and MoshiX which will do what you want and doesn't have overhead of reflection.

denniseffing commented 1 year ago

I don't understand why this issue was closed as completed as this issue wasn't fixed yet. Yes there are workarounds here but they could easily be included in kotlinx.serialization instead.

ukarlsson commented 4 months ago

This MapEncoder will do the trick of encoding to Map<String, Any>

import kotlin.collections.set
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.AbstractEncoder
import kotlinx.serialization.encoding.CompositeEncoder
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule

@OptIn(ExperimentalSerializationApi::class)
class ListEncoder : AbstractEncoder() {
    val list: MutableList<Any?> = mutableListOf()

    override val serializersModule: SerializersModule = EmptySerializersModule()

    override fun encodeValue(value: Any) {
        list.add(value)
    }

    override fun encodeFloat(value: Float) {
        list.add(value.toString().toDouble())
    }

    override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
        list.add(enumDescriptor.getElementName(index))
    }

    override fun encodeNull() {
        list.add(null)
    }

    override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
        val encoder = MapEncoder(descriptor.kind is PolymorphicKind.SEALED)
        list.add(encoder.map)
        return encoder
    }

    override fun beginCollection(
        descriptor: SerialDescriptor,
        collectionSize: Int
    ): CompositeEncoder {
        val encoder = ListEncoder()
        list.add(encoder.list)

        return encoder
    }
}

@OptIn(ExperimentalSerializationApi::class)
class MapEncoder(val sealed: Boolean = false) : AbstractEncoder() {
    override val serializersModule: SerializersModule = EmptySerializersModule()

    val map: MutableMap<String, Any?> = mutableMapOf()

    private var _key: String? = null

    private var key: String
        get() = _key!!
        set(key) {
            _key = key
        }

    fun hasKey() = _key != null

    override fun encodeValue(value: Any) {
        map[key] = value
    }

    override fun encodeFloat(value: Float) {
        map[key] = value.toString().toDouble()
    }

    override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
        map[key] = enumDescriptor.getElementName(index)
    }

    override fun encodeNull() {
        map[key] = null
    }

    override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean {
        key = descriptor.getElementName(index)
        return true
    }

    override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
        if (!hasKey()) return this

        if (sealed && key == "value") {
            return this
        }

        val encoder = MapEncoder(descriptor.kind is PolymorphicKind.SEALED)
        map[key] = encoder.map
        return encoder
    }

    override fun beginCollection(
        descriptor: SerialDescriptor,
        collectionSize: Int
    ): CompositeEncoder {
        val encoder = ListEncoder()
        map[key] = encoder.list

        return encoder
    }
}

@OptIn(ExperimentalSerializationApi::class)
fun <T> encodeToMap(serializer: SerializationStrategy<T>, value: T): Map<String, Any?> {
    val encoder = MapEncoder(sealed = serializer.descriptor.kind is PolymorphicKind.SEALED)

    encoder.encodeSerializableValue(serializer, value)
    return encoder.map
}
actor20170211030627 commented 1 month ago

I'm tryed, this method below could solute this issue in kotlin code:

@POST("app/xxx")
suspend fun appXxxRequest(@Body request: Map<String, Any>): BaseInfo<Any?>

//replace the Map to ArrayMap, TreeMap or HashMap and so on...
suspend fun appXxxRequest(@Body request: ArrayMap<String, Any>): BaseInfo<Any?>