Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.33k stars 618 forks source link

RFC: serialization delegates #661

Open pdvrieze opened 4 years ago

pdvrieze commented 4 years ago

What is your use-case and why do you need this feature? There are certain design limitations to serialization that make it hard/impossible/much less convenient to have serialization work directly on certain complex types (for example if they have bidirectional links to other objects). These objects can often be handled by custom serializers but this looses convenience, and more importantly annotations. Another option would be to use a plain datastructure (data class-ish) that intermediates the serialization.

Overall there are many cases where an intermediate helper object, purely for serialization, would be beneficial for handling more complex cases where the in-memory representation of an object does not align well with a serialization representation. While a custom serializer can theoretically handle this, it may be more beneficial to add some support for this in the plugin (and library)

Describe the solution you'd like Below is an idea of how to it may be possible to address the issue. I'm all open on implementation details, especially names.

I would like the plugin/library to support serializing the following code:

interface SerialDelegate<T> {
  fun create(): T
}

@Serializable
class ComplexClass {
  val a: String
  val b: Int
  val c: SomeChild

  @SerializeThrough
  private fun serialDelegate(): ComplexDelegate = TODO("impl omitted")

  @Serializable
  private class ComplexDelegate: SerialDelegate<ComplexClass> {
    override fun create(): ComplexClass = TODO("Create ComplexClass from delegate")
  }
}

The way I imagine this could work is: the plugin finds the @SerializeThrough annotation and based upon that will reject all other serialization information. The generated serializer has the following properties:

sandwwraith commented 4 years ago

Hi, thanks for your suggestion. I'm not sure I understood the idea. Code sample is understandable, however, it's not clear for me how it solves the stated problem: 'objects can often be handled by custom serializers but this looses convenience, and more importantly annotations'. Is SerialDelegate used instead of KSerializer? If so, where's actual serialization code (calls to Encoder/Decoder)? If not, then how would you like to use KSerializer inside of SerialDelegate?

pdvrieze commented 4 years ago

Basically (in pseudocode) the generated serializer works somewhat like the following serializer (as companion object):

    @Serializer(ComplexClass::class)
    companion object: KSerializer<ComplexClass> {

        private val delegateSerializer: KSerializer<ComplexDelegate> = ComplexDelegate.serializer()

        /**
         * Initially just a delegate descriptor, but perhaps it is possible to override some
         * container related parts
         */
        override val descriptor: SerialDescriptor
            get() = delegateSerializer.descriptor

        override fun serialize(encoder: Encoder, obj: ComplexClass) {
            delegateSerializer.serialize(encoder, obj.serialDelegate())
        }

        override fun deserialize(decoder: Decoder): ComplexClass {
            return delegateSerializer.deserialize(decoder).create()
        }

        override fun patch(decoder: Decoder, old: ComplexClass): ComplexClass {
            return delegateSerializer.patch(decoder, old.serialDelegate()).create()
        }

    }

As you see, the idea can basically be implemented already, but the idea is that it may be worthwhile to support the concept in the library. Looking at many of the issues that come in it seems to me that many people are not very confident in writing custom serializers, and in many cases the most robust way is probably to have a (private) data model purely for serialization.

The default approach to custom serializers is most likely going to be to try to do something on the original class where the problem of not being able to instantiate annotations becomes an issue when they are needed to direct formats. The workaround with a private intermediate object solves this, and it obviates the need for many custom serializers (which require understanding how serialization/deserialization and encoders/decoders work).