-
Notifications
You must be signed in to change notification settings - Fork 660
Description
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:
- The compiler does not store default values for properties anywhere in the metadata. This means it is impossible to generate the code similar to
if (prop != propDefaultValue) encoder.encodeElement(prop)
in the external serializer (Cannot generate external serializer: class is defined in another module #2512) - The compiler does not (always) store the fact that a property has a backing field. Since we aim to serialize properties with backing fields only and treat all others as transient, it can result in inconsistent serialization or descriptor information. (Descriptor of external serializer is incomplete #2549)
- Generated serializers themselves are not customizable at all. You cannot specify SerialName for a property, nor specify a serializer for a specific property (in case a serializer for its type is also external, for example: Using external serializer without UseSerializers lead to: CompilationException: Back-end (JVM) Internal error #889)
- Java classes do not have a notion of a primary constructor. As a result, it is impossible to generate a serializer for a Java class at all, which limits the usability of this feature (Unable to generate a serializer for java.time.Month #1223)
- Private or internal properties cannot be serialized correctly this way.
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”