1

I have a simple json, but the containing field has dynamic object. For instance, json can look like

{
    "fixedField1": "value1",
    "dynamicField1": {
        "f1": "abc",
        "f2": 123
    }
}

or

{
    "fixedField1": "value2",
    "dynamicField1": {
        "g1": "abc",
        "g2": { "h1": "valueh1"}
    }
}

I am trying to serialize this object, but not sure how to map the dynamic field

@Serializable
data class Response(
  @SerialName("fixedField1")
  val fixedField: String,

  @SerialName("dynamicField1")
  val dynamicField: Map<String, Any> // ???? what should be the type?
)

Above code fails with following error

Backend Internal error: Exception during code generation Cause: Back-end (JVM) Internal error: Serializer for element of type Any has not been found.

sidgate
  • 14,650
  • 11
  • 68
  • 119
  • I would suggest to use Jackson library and more specifically `JsonNode` -> example usage here https://www.baeldung.com/jackson-mapping-dynamic-object – Lukas Forst Jul 24 '19 at 10:19
  • Please take a look on `JsonObject` or `JsonElement` types. Probably it's what you are looking for. – sedovav Jul 25 '19 at 13:26

1 Answers1

4

I ran into a similar problem when I had to serialize arbitrary Map<String, Any?>

The only way I managed to do this so far was to use the JsonObject/JsonElement API and combining it with the @ImplicitReflectionSerializer

The major downside is the use of reflection which will only work properly in JVM and is not a good solution for kotlin-multiplatform.

    @ImplicitReflectionSerializer
    fun Map<*, *>.toJsonObject(): JsonObject = JsonObject(map {
        it.key.toString() to it.value.toJsonElement()
    }.toMap())

    @ImplicitReflectionSerializer
    fun Any?.toJsonElement(): JsonElement = when (this) {
        null -> JsonNull
        is Number -> JsonPrimitive(this)
        is String -> JsonPrimitive(this)
        is Boolean -> JsonPrimitive(this)
        is Map<*, *> -> this.toJsonObject()
        is Iterable<*> -> JsonArray(this.map { it.toJsonElement() })
        is Array<*> -> JsonArray(this.map { it.toJsonElement() })
        else -> {
            //supporting classes that declare serializers
            val jsonParser = Json(JsonConfiguration.Stable)
            val serializer = jsonParser.context.getContextualOrDefault(this)
            jsonParser.toJson(serializer, this)
        }
    }

Then, to serialize you would use:


val response = mapOf(
    "fixedField1" to "value1",
    "dynamicField1" to mapOf (
        "f1" to "abc",
        "f2" to 123
    )
)


val serialized = Json.stringify(JsonObjectSerializer, response.toJsonObject())

Note

This reflection based serialization is only necessary if you are constrained to use Map<String, Any?>

If you are free to use your own DSL to build the responses, then you can use the json DSL directly, which is very similar to mapOf

val response1 = json {
    "fixedField1" to "value1",
    "dynamicField1" to json (
        "f1" to "abc",
        "f2" to 123
    )
}

val serialized1 = Json.stringify(JsonObjectSerializer, response1)

val response 2 = json {
    "fixedField1" to "value2",
    "dynamicField1" to json {
        "g1" to "abc",
        "g2" to json { "h1" to "valueh1"}
    }
}

val serialized2 = Json.stringify(JsonObjectSerializer, response2)

If, however you are constrained to define a data type, and do serialization as well as deserialization you probably can't use the json DSL so you'll have to define a @Serializer using the above methods.

An example of such a serializer, under Apache 2 license, is here: ArbitraryMapSerializer.kt

Then you can use it on classes that have arbitrary Maps. In your example it would be:

@Serializable
data class Response(
  @SerialName("fixedField1")
  val fixedField: String,

  @SerialName("dynamicField1")
  @Serializable(with = ArbitraryMapSerializer::class)
  val dynamicField: Map<String, Any>
)
Mircea Nistor
  • 3,145
  • 1
  • 26
  • 32
  • 2
    Update of the ArbitaryMapSerializer.kt is here: https://github.com/uport-project/kotlin-did-jwt/blob/master/jwt/src/main/java/me/uport/sdk/jwt/model/ArbitraryMapSerializer.kt – Ben Jun 01 '20 at 10:52
  • "The major downside is the use of reflection which will only work properly in JVM and is not a good solution for kotlin-multiplatform." Why is it not a good solution for KMM? All imports are from kotlinx.serialization which is a multiplatform solution in itself: `import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive` Is there a reason that I'm not seeing? btw +1 for solution. – Goran Horia Mihail Oct 30 '20 at 11:44