If I have a class with delegated properties, is there a way to make those properties serializable?
Well, I decided to post my findings in case this helps anybody else. If there’s a better way to do this I’d love some input because this ended up being a bit more work than I was expecting.
Premise:
I have Style objects that use property delegates in order to separate calculated and explicit values. I want these style objects to be serializable where the values that were explicitly set are serialized.
All styles have a base class. That base class has a list of the properties that were provided via a style property delegate.
In my base Style class
@Serializable
@Polymorphic
abstract class StyleBase
I have a way to provide a property delegate that unsafely provides the serializer:
// NB: Property is skipped if it's not serializable
protected inline fun <reified P : Any> prop(defaultValue: P) =
StyleProp(defaultValue, P::class.serializerOrNull())
(I’d prefer this to be safe, but to have every single property provide its serializer manually would be very tedious.)
For the style property delegate, I keep a reference to the serializer and properties for the calculated and explicit values.
class StyleProp<T>(
var defaultValue: T,
val serializer: KSerializer<T>?
) : ReadWriteProperty<Style, T>
// Properties for calculatedValue, explicitValue...
Then for the serializer. This is where things felt pretty gnarly and it took a lot of code and digging through serialization source code to figure out how to solve:
class StyleSerializer<T : StyleBase>(name: String, val factory: () -> T) : KSerializer<T> {
private val default = factory()
private val serializableProps = default.allProps.filter { it.name != null && it.serializer != null }
override val descriptor = object : SerialDescriptor {
override val elementsCount: Int
get() = serializableProps.size
override val kind: SerialKind = StructureKind.OBJECT
override val serialName: String = name
override fun getElementAnnotations(index: Int): List<Annotation> {
if (!serializableProps.rangeCheck(index)) throw IndexOutOfBoundsException()
return emptyList()
}
override fun getElementDescriptor(index: Int): SerialDescriptor {
return serializableProps[index].serializer!!.descriptor
}
override fun getElementIndex(name: String): Int {
return serializableProps.indexOfFirst { it.name == name }
}
override fun getElementName(index: Int): String {
return serializableProps[index].name!!
}
override fun isElementOptional(index: Int): Boolean {
if (!serializableProps.rangeCheck(index)) throw IndexOutOfBoundsException()
return true
}
}
override fun deserialize(decoder: Decoder): T {
val compositeDecoder = decoder.beginStructure(descriptor)
val obj = factory()
while (true) {
val index = compositeDecoder.decodeElementIndex(descriptor)
if (index == CompositeDecoder.READ_DONE) break
if (!serializableProps.rangeCheck(index)) error("Unexpected index $index")
val sProp = serializableProps[index]
val prop = obj.allProps.first { it.name == sProp.name }
val value = compositeDecoder.decodeSerializableElement(descriptor, index, prop.serializer!!)
prop.explicitValue = value
}
compositeDecoder.endStructure(descriptor)
return obj
}
override fun serialize(encoder: Encoder, value: T) {
val descriptor = descriptor
val composite = encoder.beginStructure(descriptor)
for (i in 0..serializableProps.lastIndex) {
val sProp = serializableProps[i]
val prop = value.allProps.first { it.name == sProp.name }
if (prop.explicitIsSet) {
composite.encodeSerializableElement(
descriptor,
i,
prop.serializer!!,
prop.explicitValue
)
}
}
composite.endStructure(descriptor)
}
}
/**
* Adds a contextual and polymorphic style serializer to the builder.
*/
inline fun <reified T : StyleBase> SerializersModuleBuilder.styleSerializer(packageName: String, noinline factory: () -> T) {
// JS doesn't support class.qualifiedName yet.
val serializer = StyleSerializer(packageName + "." + T::class.simpleName!!, factory)
contextual(T::class, serializer)
polymorphic(StyleBase::class) {
T::class with serializer
}
}
val stylesModule = SerializersModule {
val packageName = "com.acornui.component"
styleSerializer(packageName) { BoxStyle() }
styleSerializer(packageName) { CharStyle() }
}
So yeah… I’m betting that I figured this out in the most horribly complicated way possible and someone out there will educate me on on the easy way to do this
This seems to boil down to that I have a map of property values and their corresponding serializers, but it took about one hundred lines of code to write a custom serializer for them.