kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

How do I specify a default serializer for polymorphic or sealed types

Open ForteScarlet opened this issue 1 year ago β€’ 6 comments

What is your use-case and why do you need this feature?

hello. Suppose I have an abstract type 'Command' that can be extended by third parties. I want it to have a default deserialization type target that doesn't require SerializersModule to work. It can be deserialized by any SerialFormat, not just JSON.

And I want this default to have a lower precedence than the one in SerializersModule, which means it can be overridden with SerializersModule.polymorphic(...) { defaultDeserializer { ... } }.

@Serializable
// I want something where can specify the default serialization type.
// @PolymorphicDefault(DefaultCommand::class)
sealed class Command

@Serializable
@SerialName("c1")
data class Command1(val name: String) : Command()

@Serializable
@SerialName("c2")
data class Command2(val size: Long) : Command()

// Extensible to third parties
@Serializable
abstract class CustomCommand : Command() {
    abstract val code: Int
}

@Serializable
// Maybe here?
// @PolymorphicDefault(Command::class)
data object DefaultCommand : Command()

fun decodeCommand(format: StringFormat, raw: String): Command =
    format.decodeFromString(Command.serializer(), raw)

Describe the solution you'd like

I want to be able to specify a default serializer for a polymorphic type without additional conditions (such as having to configure SerializersModule).

Maybe provide some annotations to specify a default serialisable type at compile time? Like @PolymorphicDefault in the code used as an example above.

ForteScarlet avatar Mar 06 '24 10:03 ForteScarlet

@ForteScarlet I think you are confused about SerializersModule. That is format independent, and the way to specify how polymorphic and contextual serializers are resolved. If you want a hierarchy you can apply that in the implementation for defaultDeserializer

pdvrieze avatar Mar 06 '24 10:03 pdvrieze

@pdvrieze But this Command may need to be provided for external use, so I may not be able to control their SerializersModule. I want to have a default serializer when some external consumer deserializes it.

I think I roughly know that SerializersModule has nothing to do with structure, but I saw that Content-based polymorphic deserialization seems to be able to customize a serializer to some extent with a default serializer, but it might not quite satisfy the 'wish' I mentioned, so I mentioned itπŸ˜‚

ForteScarlet avatar Mar 06 '24 10:03 ForteScarlet

There's no way to set up a default deserializer with annotation. To distribute your polymorphic serializable classes to clients, you have to distribute SerializersModule with them, so clients can extend it. SerializersModule support combination, see here: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#merging-library-serializers-modules

sandwwraith avatar Mar 11 '24 18:03 sandwwraith

Okay, but if I distribute the SerializersModule along with classes, I can't seem to provide a default serializer and allow the clients to redefine it.

    @Serializable
    abstract class Base
    @Serializable
    class T1 : Base()
    @Serializable
    class T2 : Base()

    @Test
    fun serialTest() {
        val data = "{}"
        // My distribution
        val defaultModule = SerializersModule {
            polymorphicDefaultDeserializer(Base::class) { T1.serializer() }
        }
       // Clients
        val clientModule = SerializersModule {
            include(defaultModule)
            polymorphicDefaultDeserializer(Base::class) { T2.serializer() }
            // πŸ‘† java.lang.IllegalArgumentException: Default deserializers provider for class SerTest$Base is already registered: (kotlin.String?) -> kotlinx.serialization.DeserializationStrategy<SerTest.Base>?
        }
        
        val json = Json {
            isLenient = true
            serializersModule = clientModule
        }

        val decoded = json.decodeFromString(Base.serializer(), data)
        assertIs<T2>(decoded)
    }

Does that mean I have to rely on some documentation warning or suggestion that clients can add T1 as the default serializer, but I can't give it to them directly?

ForteScarlet avatar Mar 11 '24 18:03 ForteScarlet

It seems our documentation is lacking in this place. There's a special overwriteWith (https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/overwrite-with.html) function for modules:

val defaultModule = SerializersModule {
    polymorphicDefaultDeserializer(Base::class) { T1.serializer() }
}
val clientModule = defaultModule overwriteWith SerializersModule {
    polymorphicDefaultDeserializer(Base::class) { T2.serializer() }
}

does what you want.

sandwwraith avatar Mar 12 '24 11:03 sandwwraith

@sandwwraith That's great! It seems that overwriteWith can solve that problem, thanks for letting me know πŸ˜‰

ForteScarlet avatar Mar 13 '24 16:03 ForteScarlet