1

Kotlin serialization is hard! How do I do get Kotlin to believe that the values in my properties map are either primitives or classes annotated with @Serializable?

I'm trying to turn a class like this: class Entity(val id: String, val type: String, val properties: Map<String, *>) where I know that * is primitive or @Serializable or String into JSON, e.g.: { id: "0", type: "falcon", max-flight-range-nm: 10 }

import kotlinx.serialization.*
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.encodeToJsonElement

fun main() {
    val json = Json {
        prettyPrint = true
    }
    val falcon = Entity("0", "falcon", mapOf("max-flight-range-nm" to 10))
    println(json.encodeToString(falcon))
    // Desired JSON (no nesting of 'Entity.parameters', they bump-up into parent intentionally
    // { id: "0", type: "falcon", max-flight-range-nm: 10 }
    // Avoiding extra nesting is conceptually similar to Jackson's @JsonAnyGetter annotation: https://www.baeldung.com/jackson-annotations#1-jsonanygetter
}

@Serializable
class Entity(val id: String, val type: String, @Contextual val properties: Map<String, *>) {

    @Serializer(forClass = Entity::class)
    companion object : KSerializer<Entity> {
        override fun serialize(encoder: Encoder, value: Entity) {
            encoder.encodeString(value.id)
            encoder.encodeString(value.type)
            // attempted hack to encode properties at this level
            for ((mapKey, mapValue) in value.properties) {
                val valueJson = Json.encodeToJsonElement(mapValue)
                val jsonObject = JsonObject(mapOf(mapKey to valueJson))
                encoder.encodeString(jsonObject.toString())
            }
        }
    }
}

I have full control over all code, so if needed I could go so far as to write a bunch of sealed class PropertyValue subclasses and mark them as @Serializable - so IntPropertyValue, StringPropertyValue, FooPropertyValue, etc. Maybe that's the only way?

Unfortunately the above fails at compile-time (no errors in Intelli-J though): org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error: Serializer for element of type Any? has not been found.

Jason
  • 2,579
  • 1
  • 17
  • 19
  • If this isn't possible I'll revert back to GSON or Jackson's ObjectMapper. – Jason Oct 15 '21 at 15:22
  • The problem is not about making kotlinx serialization believe anything; it does 'believe' what you are trying to do. It's telling you, correctly, it doesn't know what types to expect or how to tell them apart. You need polymorphic serialization, which is pretty well documented. Consider writing a manual JSON parser: how would you know what to serialize the values in the map as? What the library is asking you to do is define a mechanism. Using polymorphic serialization with a class discriminator is a common approach, but others are documented. – Steven Jeuris Dec 08 '21 at 21:50
  • @Jason how did you solve it eventually? can you share? – A-_-S Sep 16 '22 at 14:12
  • @AssafShouval I think I just used google's gson instead, which I found much easier to grok. – Jason Sep 20 '22 at 14:35

1 Answers1

0

I have full control over all code, so if needed I could go so far as to write a bunch of sealed class PropertyValue subclasses and mark them as @Serializable - so IntPropertyValue, StringPropertyValue, FooPropertyValue, etc. Maybe that's the only way?

You need to make PropertyValue itself @Serializable (see https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md) and of course use val properties: Map<String, PropertyValue> instead of val properties: Map<String, *> (since the error message says "Serializer for element of type Any?" you might have forgotten this step).

Also note that by default you won't get

{ id: "0", type: "falcon", max-flight-range-nm: 10 }

but e.g. (omitting key quotes)

{ id: "0", type: "falcon", properties: { 
    max-flight-range-nm: { 
        type: "your_package.IntPropertyValue", value: 10 
    } 
} }

If you want to parse/produce that particular JSON, write a custom serializer, and then you might get away with Map<String, *> too (but that's still usually a bad idea).

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487