Serializable delegates

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 :slight_smile:

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.

1 Like