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.