Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make PolymorpicSerializer serializer to work in JS #101

Closed
neworld opened this issue Mar 17, 2018 · 9 comments
Closed

Make PolymorpicSerializer serializer to work in JS #101

neworld opened this issue Mar 17, 2018 · 9 comments

Comments

@neworld
Copy link

neworld commented Mar 17, 2018

Currently PolymorpicSerializer works only in JVM.

@Whathecode
Copy link
Contributor

Is there an estimate when this would become available? I am currently looking into alternatives to deserialize JSON output from kotlinx.serialization in Typescript/JS. If this were planned to be released soon I might postpone this instead.

@bartblast
Copy link

@sandwwraith Can you guys let us know whether anybody is working on it and/or give us a very very rough ETA? I believe a lot of people depend on that feature and I'm thinking about writing my own hacky, simplified serialization lib if polymorphic serializers wont' work in kotlinx serialization soon. But I need to know whether it makes sense to wait or invest time on writing something on my own... Thanks!

@Whathecode
Copy link
Contributor

Whathecode commented Aug 7, 2018

In multiplatform projects, JavaScript compilation fails when applying @Serializable attributes to polymorph class hierarchies.

Assuming polymorph JavaScript serialization will be supported in the future, I still want to be able to apply @Serializable to classes in a 'common' module. For now I am fine with serialization solely working when targeting the JVM.

To not break compilation, I do the following. In common:

@Target( AnnotationTarget.PROPERTY, AnnotationTarget.CLASS )
expect annotation class Serializable()

@Target( AnnotationTarget.PROPERTY, AnnotationTarget.CLASS )
expect annotation class SerializableWith( val with: KClass<out KSerializer<*>> )

In JS:

// Serialization is not yet supported on JavaScript runtime. These annotations do nothing.
actual annotation class Serializable
actual annotation class SerializableWith actual constructor( actual val with: KClass<out KSerializer<*>> )

In JVM:

actual typealias Serializable = kotlinx.serialization.Serializable
actual typealias SerializableWith = kotlinx.serialization.Serializable

The only way I could get the with parameter to work was by defining two separate expect definitions. I do not know why, but reported this as a potential bug.

Perhaps this is of use to others.

@pdvrieze
Copy link
Contributor

pdvrieze commented Aug 7, 2018

Javascript has a number of limitations:

  • It does not support annotations - this could be emulated but a standard way would be very beneficial, it has to happen at kotlin compiler level
  • There is almost no reflection support - PolymorphicSerializer in Java uses Class.forName to get the class. This API is not available in Javascript. In general javascript reflection is very limited for Kotlin. It may be possible to write related code to load a prototype in Javascript, but the problem remains that annotations are not supported so the deserializer cannot determine the correct deserializer to use for the dynamically determined child.
  • An alternative would be to enumerate all possible subtypes at compiletime and then not need to use reflection.

@Whathecode
Copy link
Contributor

Whathecode commented Sep 29, 2018

I got polymorph serialization up and running in JavaScript for my particular use case (in line with the alternative suggested by @pdvrieze). It relies on specifying custom serializers on class members which need to be serialized as polymorph. Serializing objects as polymorph is thus explicit rather than implicit. Otherwise, JavaScript compilation warnings occur if no explicit serializer is specified. This seems very much in line with the design proposal for Kotlin 1.3.

Polymorphic serialization is never enabled automatically. To enable this feature, use @SerializableWith(PolymorphicSerializer::class) or just @Polymorphic on the property.

To make this work with JavaScript I simply adopted the existing PolymorphicSerializer and added functions through which polymorph types and their serializers can be registered. The full class can be found here.

object PolymorphicSerializer : KSerializer<Any>
{
    override val serialClassDesc: KSerialClassDesc = PolymorphicSerializerClassDesc

    private val simpleNameSerializers = mutableMapOf<String, KSerializer<Any>>()
    private val qualifiedSerializers = mutableMapOf<String, KSerializer<Any>>()

    fun <T: Any> registerSerializer( klass: KClass<T>, qualifiedName: String ) { ... }
    private fun getSerializerBySimpleClassName( className: String ): KSerializer<Any> { ... }
    private fun getSerializerByQualifiedName( qualifiedName: String ): KSerializer<Any> { ... }
    ...
}

Subsequently, in save() and load() serializers are retrieved from the map of registered serializers instead of serializerBySerialDescClassname<Any>(klassName, input.context) which is the only call which does not work for JS since it relies on retrieving the fully qualified name which cannot be retrieved due to JS reflection limitations.

This serializer can be used as follows (full examples in unit tests).

@Serializable
internal class A( val a: String = "a" ) : BaseClass()
{
    companion object
    {
        init { PolymorphicSerializer.registerSerializer( A::class, "dk.cachet.carp.protocols.domain.serialization.PolymorphicSerializerTest.A" ) }
    }
}

@Serializable
class Nested(
    @Serializable( PolymorphicSerializer::class )
    val field: BaseClass ) { }

For lists, the following serializer can be specified: object PolymorphicArrayListSerializer : KSerializer<List<Any>> by ArrayListSerializer( PolymorphicSerializer )

The primary limitation of this approach is that you cannot register classes with the same name. Less severe is the extra step of having to register serializers manually in any polymorph type. However, the big benefit is the same codebase can now be used to serialize from/to Java and JavaScript. After having looked into some JavaScript serialization libraries which also do not support proper polymorph serialization, I am quite pleased with this result, and believe an approach like this is much preferred (for maintainability/scalability) over trying to do something similar outside of Kotlin.

@sandwwraith
Copy link
Member

#394 has this and it is included into 0.11.0 release.

@Whathecode
Copy link
Contributor

Great! Is there a readme on how to use this yet? I can't seem to find it.

@Whathecode
Copy link
Contributor

For those looking for information: registering polymorphic types needs to be done using SerialModule which is passed to the Json constructor. More info can be found here: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/custom_serializers.md#registering-and-context

An example on how to initialize SerialModule using a builder to register a couple of polymorphic types can be found here: https://medium.com/@andreclassen1337/goodbye-runtimetypeadapterfactory-polymorphic-serialization-using-kotlinx-serialization-46a8cec36fdc

@pdvrieze
Copy link
Contributor

One caveat here that has hit me. The lookup now uses the name property of the serialdescriptor instead of the class name (old behaviour). While the default value for this name is the type name, this can be overridden by specifying @SerialName on the type declaration. In that case, the serialization is not compatible anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants