Skip to content

Proposal: Deprecate for removal the ‘External serializers’ feature with the aim of being replaced by the new 'Serializers DSL' #3084

@sandwwraith

Description

@sandwwraith

Why?

The external serializers feature is incomplete and has a lot of bugs. The leading causes for this are the fundamental technical limitations of the Kotlin compiler and overall design, such as:

Replacement

Replacement solution requirements:

  • The user should be able to specify the serial name, serializer, and default value for the property.
  • The user should be able to specify which properties are serializable and which are not, as the compiler cannot deduce it for them.
  • The user should be able to specify which constructor to call, as we cannot deduce it for them.

We can see that the ideal solution here is just to write a custom manual serializer. However, it requires significant expertise in kotlinx.serialization, and serializers themselves are boilerplate-ish, since Encoders API is designed mainly to be called from code generated by the compiler plugin. Perhaps AI can generate something reasonable, but we shouldn’t rely entirely on it.

A good example of avoiding writing a manual serializer is JsonTransformingSerializer, which is very popular and solves a lot of pain points. Providing a specialized solution for this particular problem also seems viable. Let’s name it Serializers DSL for now.

Draft can look like this:

class Example(val s: String, val cnt: Int = 0)

object Ex: AbstractSerializer<Example>() {
  override val serializableProperties = properties(
    Property("s", serializer = String.serializer()),
    Property("counter", serializer = Int.serializer(), optional = true, defaultValue = 0)
  )

  override fun deconstruct(obj: Example) {
     obj.s serializeAs properties["s"]

     obj.cnt serializeAs properties["counter"]
  }

  override fun construct(data: Properties): Example {
    return Example(data[properties["s"]], data[properties["counter"]])
  }
}
    

Or like this:

class Example(val s: String, val cnt: Int = 0)

object Ex: AbstractSerializer<Example>() {
  override val schema = properties(
    Property("s", serializer = String.serializer()) { it.s },
    Property("counter", serializer = Int.serializer(), optional = true, defaultValue = 0) { it.cnt}
  ).constructor { p1, p2 -> Example(p1, p2) }
}
    

It is also possible to employ a partial generation of code by the plugin:

class Example(val s: String, val cnt: Int = 0) // 3rd party module

abstract class AbstractSerializer<T>: KSerializer<T> {
  override val descriptor get() = shema.toDescriptor()
  abstract val schema: Properties
}

@ExternalSerializer
object Ex: AbstractSerializer<Example>() {
  override val schema = properties(
    Property("s", serializer = String.serializer(), Example::s),
    Property("counter", serializer = Int.serializer(), optional = true, defaultValue = 0, Example::cnt),
    Creator(::Example)
  )

  // generated by plugin:

  fun serialize(e: Example, enc: Encoder) {
    enc.encode(schema[0], e.s)
    encoder.encode(schema[1], e.cnt)
  }

  
}

Concerns

  • Serializers for non-standard classes (enum, sealed) should also be available to users. This means that we also have to design APIs for them (currently lacking, such serializers are implementation details).

  • Such DSL with lambdas is slower by definition, less performant than its manually written counterpart. Users may blame our design for this. Recommendation for performance problems will be “replace with manually written serializer via Encoders API”

Metadata

Metadata

Assignees

No one assigned

    Labels

    External generated serializerEverything related to external auto-generated serializer (@Serializer annotation)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions