kmongo icon indicating copy to clipboard operation
kmongo copied to clipboard

Kotlinx serialization favors sealed base serializer even when explicitly using subclass

Open trema96 opened this issue 4 years ago • 5 comments

Problem description

When using kotlinx serialization if a type implements a sealed interface kmongo will try to serialize it using the interface serializer even if the compile-time type of the object is an implementation of the aforementioned interface.

For example if i have the following types

sealed interface Foo {
    @Serializable
    data class FooA(val x: Int) : Foo
    @Serializable
    data class FooB(val x: Double) : Foo
    @Serializable
    data class FooC(val z: String) : Foo
}

and i attempt to insert an instance of FooA in a collection of only FooAs

client.getDatabase("testDb").getCollection<FooA>("fooAs").insertOne(FooA(1))

i get the error

Exception in thread "main" kotlinx.serialization.SerializationException: Class 'FooA' is not registered for polymorphic serialization in the scope of 'Foo'.

Note that this does not happen if Foo is not sealed, or if FooA is not the top-level document, as in the following example

@Serializable
data class FooContainer(val foo: FooA)

client.getDatabase("testDb").getCollection<FooContainer>("fooContainers").insertOne(FooContainer(FooA(1))) //Ok

Cause

The issue seems to be in the implementation of KMongoSerializationRepository.getSerializer which is used to get the serializer when inserting documents and favors the superclass serializer.

Workaround

Explicitly specifying the serializer of the various Foo implementations like in the following example solves the issue

registerSerializer(FooA.serializer())
registerSerializer(FooB.serializer())
registerSerializer(FooC.serializer())

Adding a module that specifies contextual serializers for the implementations of Foo also works.

trema96 avatar Dec 10 '21 14:12 trema96

Since https://github.com/Kotlin/kotlinx.serialization/issues/1576 fix you can use the @Serializable annotation on sealed interface. But there is still an issue when accessing serializer from KClass (https://github.com/Kotlin/kotlinx.serialization/issues/1869).

Workaround for now: use sealed class or define custom PolymorphicSerializer

zigzago avatar May 13 '22 08:05 zigzago

Sorry, I did not explain myself well.

The issue is that I would expect FooA : Foo to be serialized in two different ways when it is added in a collection of Foo vs a collection of FooA: in a collection of Foo it must include the type information, while in a collection of FooA it should not. For example in a collection of various Foo I want something like this

[
  {
      "_id": { "$oid": "627e215005eed8558ea6f3c3" },
      "___type": "Foo.FooA",
      "x": 1
  },
  {
      "_id": { "$oid": "627e215005eed8558ea6f3c5" },
      "___type": "Foo.FooC",
      "z": "a"
  }
]

and in a collection of only FooA I want

[
  {
      "_id": { "$oid": "627e215005eed8558ea6f3c3" },
      "x": 1
  },
  {
      "_id": { "$oid": "627e215005eed8558ea6f3c5" },
      "x": 2
  }
]

I would say that a situation where I want both of these cases is unlikely, but in my case I wanted to have a separate collection for each of FooA, FooB and FooC, yet they needed to implement a sealed interface for other reasons.

By allowing the serialization of Foo, using only your suggested workarounds (sealed class or polymorphic serializer) I also get the superfluous type information in the collection of only FooA ("___type": "Foo.FooA"). Using registerSerializer as I suggested, instead, this field is omitted. You can combine both of these workarounds to obtain the desired behavior with collections of Foo and FooA.

As I previously mentioned I think the problem is that the implementation of KMongoSerializationRepository.getSerializer always uses the serializer of the superclass of T if it is sealed. What I think should happen is that if I'm serializing an instance of FooA in a collection of Foos it should use the serializer for Foo, while if I'm serializing it in a collection of FooAs it should use the serializer of FooA.

trema96 avatar May 13 '22 09:05 trema96

Thank you for the explanation.

Unfortunately the change would not be backward compatible. It would be also problematic for serialization without collection context:

FooA(1).bson // -> do we use Foo or FooA serializer?

May be it would be also counter-intuitive:


database.getCollection<FooA>("foo").insert(FooA(2))
database.getCollection<Foo>("foo").find() // -> fail at runtime

database.getCollection<Foo>("foo2").insert(FooA(1))
database.getCollection<FooB>("foo2").find() // -> fail at runtime

Though I agree with your remarks, the fix is not simple - it would need backward compatible flag & custom serializer for bson/json serialization - I think we are going to stay with your workaround :(

zigzago avatar May 13 '22 11:05 zigzago

I imagined it would not be simple to change this behavior, I mostly opened this issue in hope it would help if someone else incurs in the same problem. In case should we change the title back to something that better summarize the issue? In general the problem is with any sealed supertype (class or interface).

Anyway thank you for this great tool!

trema96 avatar May 13 '22 11:05 trema96

Than you for reporting the issue !

zigzago avatar May 13 '22 13:05 zigzago