summaryrefslogtreecommitdiff
path: root/runtime/commonMain/src/kotlinx/serialization/Polymorphic.kt
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/commonMain/src/kotlinx/serialization/Polymorphic.kt')
-rw-r--r--runtime/commonMain/src/kotlinx/serialization/Polymorphic.kt151
1 files changed, 151 insertions, 0 deletions
diff --git a/runtime/commonMain/src/kotlinx/serialization/Polymorphic.kt b/runtime/commonMain/src/kotlinx/serialization/Polymorphic.kt
new file mode 100644
index 00000000..386b1349
--- /dev/null
+++ b/runtime/commonMain/src/kotlinx/serialization/Polymorphic.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization
+
+import kotlinx.serialization.PolymorphicClassDescriptor.kind
+import kotlinx.serialization.internal.*
+import kotlinx.serialization.modules.*
+import kotlin.reflect.*
+
+/**
+ * A [SerialDescriptor] for polymorphic serialization with special kind.
+ *
+ * Currently, it has no guarantees neither on its reference transparency nor its [elementDescriptors], only on [kind].
+ */
+public object PolymorphicClassDescriptor : SerialClassDescImpl("kotlin.Any") {
+ public override val kind: SerialKind = UnionKind.POLYMORPHIC
+
+ init {
+ // serial ids would be assigned automatically, since
+ // we decided not to support @SerialInfo annotations
+ // in custom serializers code
+ addElement("class")
+ addElement("value")
+ }
+}
+
+/**
+ * This class provides support for multiplatform polymorphic serialization.
+ * Due to security and reflection usage concerns, all serializable implementations of some abstract class must be registered in advance.
+ * However, it allows registering subclasses in runtime, not compile-time. For example, it allows adding additional subclasses to the registry
+ * that were defined in a separate module, dependent on the base module with the base class.
+ *
+ * Polymorphic serialization is enabled automatically by default only for types that are interfaces or [Serializable] abstract classes.
+ * To enable this feature explicitly on other types, use `@SerializableWith(PolymorphicSerializer::class)`
+ * or just [Polymorphic] annotation on the property.
+ *
+ * Another security requirement is that we only allow to register subclasses in the scope of a [baseClass]
+ * The motivation for this is easily understandable from the example:
+ *
+ * ```
+ * abstract class BaseRequest()
+ * @Serializable data class RequestA(val id: Int): BaseRequest()
+ * @Serializable data class RequestB(val s: String): BaseRequest()
+ *
+ * abstract class BaseResponse()
+ * @Serializable data class ResponseC(val payload: Long): BaseResponse()
+ * @Serializable data class ResponseD(val payload: ByteArray): BaseResponse()
+ *
+ * @Serializable data class Message(
+ * @Polymorphic val request: BaseRequest,
+ * @Polymorphic val response: BaseResponse
+ * )
+ * ```
+ * In this example, both request and response in `Message` are serializable with [PolymorphicSerializer].
+ *
+ * `BaseRequest` and `BaseResponse` are base classes and they are captured during compile time by the plugin.
+ * Yet [PolymorphicSerializer] for `BaseRequest` should only allow `RequestA` and `RequestB` serializers, and none of the response's serializers.
+ *
+ * This is achieved via special registration function in the module:
+ * ```
+ * val requestAndResponseModule = SerializersModule {
+ * polymorphic(BaseRequest::class) {
+ * RequestA::class with RequestA.serializer()
+ * RequestB::class with RequestB.serializer()
+ * }
+ * polymorphic(BaseResponse::class) {
+ * ResponseC::class with ResponseC.serializer()
+ * ResponseD::class with ResponseD.serializer()
+ * }
+ * }
+ * ```
+ *
+ * By default (without special support from [Encoder]), polymorphic values are serialized as list with
+ * two elements: fully-qualified class name (String) and the object itself.
+ *
+ * @see SerializersModule
+ * @see SerializersModuleBuilder.polymorphic
+ */
+public class PolymorphicSerializer<T : Any>(private val baseClass: KClass<T>) : KSerializer<Any> {
+ public override val descriptor: SerialDescriptor = PolymorphicClassDescriptor
+
+ @Suppress("UNCHECKED_CAST")
+ public override fun serialize(encoder: Encoder, obj: Any) {
+ val actualSerializer = findPolymorphicSerializer(encoder, obj)
+ val compositeEncoder = encoder.beginStructure(descriptor)
+ compositeEncoder.encodeStringElement(descriptor, 0, actualSerializer.descriptor.name)
+ compositeEncoder.encodeSerializableElement(descriptor, 1, actualSerializer as KSerializer<Any>, obj)
+ compositeEncoder.endStructure(descriptor)
+ }
+
+ public override fun deserialize(decoder: Decoder): Any {
+ val compositeDecoder = decoder.beginStructure(descriptor)
+ var klassName: String? = null
+ var value: Any? = null
+ mainLoop@ while (true) {
+ when (val index = compositeDecoder.decodeElementIndex(descriptor)) {
+ CompositeDecoder.READ_ALL -> {
+ klassName = compositeDecoder.decodeStringElement(descriptor, 0)
+ val serializer = findPolymorphicSerializer(compositeDecoder, klassName)
+ value = compositeDecoder.decodeSerializableElement(descriptor, 1, serializer)
+ break@mainLoop
+ }
+ CompositeDecoder.READ_DONE -> {
+ break@mainLoop
+ }
+ 0 -> {
+ klassName = compositeDecoder.decodeStringElement(descriptor, index)
+ }
+ 1 -> {
+ klassName = requireNotNull(klassName) { "Cannot read polymorphic value before its type token" }
+ val serializer = findPolymorphicSerializer(compositeDecoder, klassName)
+ value = compositeDecoder.decodeSerializableElement(descriptor, index, serializer)
+ }
+ else -> throw SerializationException("Invalid index in polymorphic deserialization of " +
+ "${klassName ?: "unknown class"} with base $baseClass" +
+ "\n Expected 0, 1, READ_ALL(-2) or READ_DONE(-1), but found $index")
+ }
+ }
+
+ compositeDecoder.endStructure(descriptor)
+ return requireNotNull(value) { "Polymorphic value have not been read for class $klassName" }
+ }
+
+ /**
+ * Lookups a polymorphic serializer by given [klassName] in the context of [decoder] by the current [base class][baseClass].
+ * Throws [SerializationException] if serializer is not found.
+ */
+ public fun findPolymorphicSerializer(
+ decoder: CompositeDecoder,
+ klassName: String
+ ): KSerializer<out T> = decoder.context.getPolymorphic(baseClass, klassName)
+ ?: throwSubtypeNotRegistered(klassName, baseClass)
+
+ /**
+ * Lookups a polymorphic serializer by given [value] in the context of [encoder] by the current [base class][baseClass].
+ * Throws [SerializationException] if serializer is not found.
+ */
+ @Suppress("UNCHECKED_CAST")
+ public fun findPolymorphicSerializer(
+ encoder: Encoder,
+ value: Any
+ ): KSerializer<out T> = encoder.context.getPolymorphic(baseClass, value as T) ?: throwSubtypeNotRegistered(value::class, baseClass)
+}
+
+
+private fun throwSubtypeNotRegistered(subClassName: String, baseClass: KClass<*>): Nothing =
+ throw SerializationException("$subClassName is not registered for polymorphic serialization in the scope of $baseClass")
+
+private fun throwSubtypeNotRegistered(subClass: KClass<*>, baseClass: KClass<*>): Nothing = throwSubtypeNotRegistered(subClass.toString(), baseClass)