Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to convert map into json string #746

Closed
roths opened this issue Mar 9, 2020 · 34 comments
Closed

How to convert map into json string #746

roths opened this issue Mar 9, 2020 · 34 comments
Labels

Comments

@roths
Copy link

roths commented Mar 9, 2020

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))

@roths roths added the feature label Mar 9, 2020
@sandwwraith
Copy link
Member

Use JsonObject instead of Map, see #296

@gabin8
Copy link

gabin8 commented Mar 13, 2020

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

@altavir
Copy link

altavir commented Mar 13, 2020

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

@roths
Copy link
Author

roths commented Mar 15, 2020

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
Copy link
Member

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

@gabin8
Copy link

gabin8 commented Mar 20, 2020

@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
Copy link

francos commented May 1, 2020

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 #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
Copy link

cpboyd commented Jul 15, 2020

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
Copy link

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

@francos
Copy link

francos commented Oct 8, 2020

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

@aids61517
Copy link

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
Copy link
Member

qwwdfsad commented Oct 9, 2020

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

@aids61517
Copy link

aids61517 commented Oct 9, 2020

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
Copy link

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
Copy link

Any news for this issue?

@markchristopherng
Copy link

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
Copy link
Member

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

@markchristopherng
Copy link

@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
Copy link

WontakKim commented Dec 2, 2020

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
Copy link

+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
Copy link

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
Copy link

dariuszkuc commented Feb 16, 2021

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?

beepsoft added a commit to dsd-sztaki-hu/hasura-subset that referenced this issue Feb 23, 2021
Used kotlinx.serialization although it makes it unnecessarily difficult
to convert Converting Map<String, Any> to JSON.

Used this solution:

Kotlin/kotlinx.serialization#746 (comment)
@dariuszkuc
Copy link

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
Copy link

neumannk commented Mar 6, 2021

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
Copy link

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.

@teble
Copy link

teble commented Jun 17, 2021

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
Copy link

nkbai commented Aug 16, 2021

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
Copy link

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
Copy link
Member

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

@emartynov
Copy link

emartynov commented Jan 25, 2022

@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
Copy link

@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
Copy link

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
Copy link

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
Copy link

actor20170211030627 commented Sep 24, 2024

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?>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests