Closed roths closed 2 years ago
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 Map
s 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<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.
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)
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
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:
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.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.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())
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.
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?
@emartynov Then it is a separate issue, you can create new ticket with full stacktrace & reproducer
@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 👆🏼.
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.
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
}
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 theMapto ArrayMap, TreeMap or HashMap and so on... suspend fun appXxxRequest(@Body request: ArrayMap<String, Any>): BaseInfo<Any?>
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))