2

I want to serialize Map<String, Any> and one of the values type is Pair<Int, Int>. How to register the Pair as polymorphic subclass for that?

val module = SerializersModule {
    polymorphic(Any::class) {
        subclass(Int::class, PolymorphicPrimitiveSerializer(Int.serializer()))
        subclass(String::class, PolymorphicPrimitiveSerializer(String.serializer()))
        subclass(Pair::class, PolymorphicSerializer(Pair::class))
    }
}
val format = Json { serializersModule = module }
val mm = mapOf<String, Any>()
        .plus("int-int pair") to (5 to 10))
val jsoned = format.encodeToString(mm)
val mmDecoded = format.decodeFromString(jsoned)
require(mm==mmDecoded)

should encode to json like:

[{"first": "int-int pair", 
"second":{"type": "Pair", "value": 
  {"first": {"type": Int, "value":5}, "second": {"type":Int, "value": 10}}}}]

But produce the following error:

Exception in thread "main" java.lang.ExceptionInInitializerError Caused by: java.lang.IllegalArgumentException: Serializer for Pair can't be registered as a subclass for polymorphic serialization because its kind OPEN is not concrete. To work with multiple hierarchies, register it as a base class. at kotlinx.serialization.json.internal.PolymorphismValidator.checkKind(PolymorphismValidator.kt:41) at kotlinx.serialization.json.internal.PolymorphismValidator.polymorphic(PolymorphismValidator.kt:31) at kotlinx.serialization.modules.SerialModuleImpl.dumpTo(SerializersModule.kt:189) at kotlinx.serialization.json.JsonImpl.validateConfiguration(Json.kt:358) at kotlinx.serialization.json.JsonImpl.(Json.kt:352) at kotlinx.serialization.json.JsonKt.Json(Json.kt:189) at kotlinx.serialization.json.JsonKt.Json$default(Json.kt:185) at MainKt.(Main.kt:143)

A-_-S
  • 680
  • 5
  • 21
  • Polymorphic subclass serialization is not related to serializing `Map`. The question is what's your expected output after serializing the map in your qustion? – user3738870 Nov 02 '22 at 15:29
  • the expected output is that the deserialized would be the same as the original object. i.e. deserialized(serializeed(mm))==mm – A-_-S Nov 02 '22 at 15:33
  • @user3738870 I've added the demmand to the code as well – A-_-S Nov 02 '22 at 16:01
  • @user3738870, also about your note on Map it is related because I'm serializing a Generic type inside other Generic type – A-_-S Nov 02 '22 at 16:40
  • I understand, but what would you like an int pair to look like after serialization? For example, if you have `5 to 10`, would it be `{"first": 5, "second": 10}` or `[5, 10]` or `{"5": 10}` or something else? – user3738870 Nov 03 '22 at 13:24
  • @user3738870, It must be a way that it could be recognized when deserializing the Map. I don't care about the readability of the json. The actual map, I'm serializing as a List of Pairs, so it could be something like `[{"first": "int-int pair", "second":{"type": "Pair", value: {"first": {type: Int, value:5}, "second": {type:Int, value: 10}}}}]` – A-_-S Nov 03 '22 at 16:18

3 Answers3

0

I could not make it work with polymorphic subclasses, but I don't think that feature is intended for use with Any and primitives anyway (see this question). A custom serializer seems like a more appropriate and simpler solution and unlike the polymorphic serialization, it doesn't require too much custom serializer code:

@ExperimentalSerializationApi
class DynamicLookupSerializer: KSerializer<Any> {
    override val descriptor: SerialDescriptor = ContextualSerializer(Any::class, null, emptyArray()).descriptor

    @OptIn(InternalSerializationApi::class)
    override fun serialize(encoder: Encoder, value: Any) {
        val actualSerializer = encoder.serializersModule.getContextual(value::class) ?: value::class.serializer()
        encoder.encodeSerializableValue(actualSerializer as KSerializer<Any>, value)
    }

    override fun deserialize(decoder: Decoder): Any {
        return try {
            PairSerializer(Int.serializer(), Int.serializer()).deserialize(decoder)
        } catch (e: Throwable) {
            try {
                decoder.decodeInt()
            } catch (e: Throwable) {
                decoder.decodeString()
            }
        }
    }
}

val module = SerializersModule {
    contextual(Any::class, DynamicLookupSerializer())
    contextual(Pair::class) {
        PairSerializer(Int.serializer(), Int.serializer())
    }
}
val format = Json { serializersModule = module }
val mm = mapOf<String, Any>()
    .plus("int-int pair" to (5 to 10))
    .plus("int" to 6)
    .plus("string" to "some string")
    .plus("another-int" to 86248726)
    .plus("another-pair" to (56 to 961))
val jsoned = format.encodeToString(mm)
println(jsoned)
val mmDecoded = format.decodeFromString<Map<String, Any>>(jsoned)
require(mm==mmDecoded)

In this custom serializer, we find the actual serializer for value: Any when serializing by looking it up via its class (value::class). As a result, PairSerializer(Int.serializer(), Int.serializer()) has to be registered too so that it will be found in DynamicLookupSerializer.serialize. When deserializing, we try the supported serializers one by one (string, int, and pair of ints).

I do realize that this is not the nicest solution due to the try-catches but it does work and it's simple enough.

user3738870
  • 1,415
  • 2
  • 12
  • 24
  • why do you think that polymorphic sublclasses aren't intended to work with Any? what is the advantage of using Contextual? – A-_-S Nov 08 '22 at 08:39
  • I've added some explanation about those questions to my answer. – user3738870 Nov 08 '22 at 09:13
  • I read the question in the link. I still don't see the advantages of contextual over poylmorphic. you could use this to spare the writing of the code: https://github.com/assafshouval/KtPolymorphicPrimitiveSerializer/blob/34386879e9531c098413abc45039ba60c714bb9b/PolymorphicPrimitiveSerializer.kt – A-_-S Nov 08 '22 at 11:32
  • Well, the simplest advantage is that this one actually works, while polymorphic doesn't. – user3738870 Nov 08 '22 at 11:47
0

I solved it by providing custom Polymorphic serializer similar to how I serialized the map: https://github.com/assafshouval/PolymorphicMapSerializer/blob/master/src/main/kotlin/Main.kt

A-_-S
  • 680
  • 5
  • 21
0
import kotlinx.serialization.builtins.*
val pairAnyAnySerializer = PairSerializer(
    PolymorphicSerializer(Any::class), PolymorphicSerializer(Any::class)
    ) as <KSerializer<Pair<*,*>>>()

and for every type that is defined in the serializers module for polymorphic serializatin of Any it will serialize/deserialze correct.

val json = Json {
    serializersModule = SerializersModule {
        polymorphic(Any::class) {
            subclass(String::class, PolymorphicPrimitiveSerializer(String.serializer()))
            subclass(Int::class, PolymorphicPrimitiveSerializer(Int.serializer()))
        }
    }
}
A-_-S
  • 680
  • 5
  • 21