Skip to content

Commit

Permalink
Make SerialModule sealed class instead of interface (#726)
Browse files Browse the repository at this point in the history
    * As explained in #697, it appears that concatenating two modules as "behaviours" (as opposed to "bag of serializers") is way too dangerous for the end-users and implies a lot of inconsistent behaviours and performance hits. So the most rational solution here is to prohibit such concatenations by closing the way to implement your own SerialModule
    * In order to still solve "fallback serializer" use-case, new 'default' DSL is introduced along with its support in PolymorphicSerializer

Fixes #697
Fixes #448
  • Loading branch information
qwwdfsad committed Mar 23, 2020
1 parent 44215c3 commit a94f6f3
Show file tree
Hide file tree
Showing 17 changed files with 306 additions and 116 deletions.
46 changes: 39 additions & 7 deletions docs/polymorphism.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Introduction

Polymorphic serialization is usually a very complicated and dangerous feature due to the amount of reflection it brings
Polymorphic serialization is usually a complicated and dangerous feature due to the amount of reflection it brings
and security concerns you should address in your application
(like "what if you accidentally load or deserialize a class that is not allowed to be in this part of the program").

Expand All @@ -22,15 +22,17 @@ compiler plugin can enumerate them automatically (see more in section [Sealed cl

## Table of contents

* [Basic case](#basic-case)
* [A bit of customizing](#a-bit-of-customizing)
* [Quick start](#quick-start)
* [Customization](#customization)
+ [Class name](#class-name)
+ [Default serializer](#default-serializer)
* [Differences for interfaces, abstract and open classes](#differences-for-interfaces-abstract-and-open-classes)
* [Sealed classes](#sealed-classes)
+ [Sealed classes: before 0.14.0](#sealed-classes-before-0140)
* [Complex hierarchies with several base classes](#complex-hierarchies-with-several-base-classes)
* [A word for multi-project applications and library developers](#a-word-for-multi-project-applications-and-library-developers)

## Basic case
## Quick start

Let's break down a basic case with a simple class hierarchy:

Expand All @@ -50,8 +52,8 @@ To be able to serialize and deserialize both `StringMessage` and `IntMessage`, w
```kotlin
val messageModule = SerializersModule { // 1
polymorphic(Message::class) { // 2
StringMessage::class with StringMessage.serializer() // 3
IntMessage::class with IntMessage.serializer() // 4
subclass<StringMessage>() // 3
subclass<IntMessage>() // 4
}
}
```
Expand Down Expand Up @@ -90,8 +92,9 @@ Such an approach works on JVM, JS, and Native without reflection (only with `KCl

> Pro tip: to use `Message` without a wrapper, you can pass `PolymorphicSerializer(Message::class)` to parse/stringify.
## A bit of customizing
## Customization

### Class name
By default, encoded _type name_ is equal to class' fully-qualified name. To change that, you can annotate the class with `@SerialName` annotation:

```kotlin
Expand Down Expand Up @@ -125,6 +128,35 @@ json.stringify(MessageWrapper.serializer(), MessageWrapper(IntMessage(121)))

> Note: this form is default and can't be changed for formats that do not support polymorphism natively, e.g., Protobuf.
### Default serializer

It is possible to register a factory of default serializers, e.g. in order to return error object in
case of unknown subclasses or to migrate API from one version to another.

The following module allows us to deserialize polymorphically both JSON with `successful_response_v2` and `successful_response_v3`
as its type discriminator:

```kotlin
abstract class ApiResponse

@SerialName("successful_response_v3")
class SuccessfulApiResponse(val code: Int) : ApiResponse()

val responseModule = SerializersModule {
polymorphic(ApiResponse::class) {
subclass<SuccessfulApiResponse>()
default { className ->
if (className == "successful_response_v2") SuccessfulApiResponse.serializer() // 1
else null
}
}
}
```

Line `1` implies that objects of type `successful_response_v3` and `successful_response_v2` have the same serialized form,
though nothing prevents us to return a different serializer here, potentially customized with runtime behaviour, such as
[JsonParametricSerializer](json_transformations.md#json-parametric-polymorphic-deserialization) .

## Differences for interfaces, abstract and open classes

As you know, interfaces and abstract classes can't be instantiated.
Expand Down
8 changes: 4 additions & 4 deletions runtime/commonMain/src/kotlinx/serialization/Polymorphic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ public val PolymorphicClassDescriptor: SerialDescriptor get() = error("This prop
* ```
* val requestAndResponseModule = SerializersModule {
* polymorphic(BaseRequest::class) {
* subclass<RequestA>
* subclass<RequestB>
* subclass<RequestA>()
* subclass<RequestB>()
* }
* polymorphic(BaseResponse::class) {
* subclass<ResponseC>
* subclass<ResponseD>
* subclass<ResponseC>()
* subclass<ResponseD>()
* }
* }
* ```
Expand Down
10 changes: 5 additions & 5 deletions runtime/commonMain/src/kotlinx/serialization/SealedSerializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ import kotlin.reflect.*
* ```
* val abstractContext = SerializersModule {
* polymorphic(ProtocolWithAbstractClass::class, ProtocolWithAbstractClass.Message::class) {
* ProtocolWithAbstractClass.Message.IntMessage::class with ProtocolWithAbstractClass.Message.IntMessage.serializer()
* ProtocolWithAbstractClass.Message.StringMessage::class with ProtocolWithAbstractClass.Message.StringMessage.serializer()
* subclass<ProtocolWithAbstractClass.Message.IntMessage>()
* subclass<ProtocolWithAbstractClass.Message.StringMessage>()
* // no need to register ProtocolWithAbstractClass.ErrorMessage
* }
* }
Expand Down Expand Up @@ -108,11 +108,11 @@ public class SealedClassSerializer<T : Any>(
}.mapValues { it.value.value }
}

override fun findPolymorphicSerializer(decoder: CompositeDecoder, klassName: String): KSerializer<out T> {
override fun findPolymorphicSerializer(decoder: CompositeDecoder, klassName: String): DeserializationStrategy<out T> {
return serialName2Serializer[klassName] ?: super.findPolymorphicSerializer(decoder, klassName)
}

override fun findPolymorphicSerializer(encoder: Encoder, value: T): KSerializer<out T> {
return class2Serializer[value::class] ?: super.findPolymorphicSerializer(encoder, value)
override fun findPolymorphicSerializer(encoder: Encoder, value: T): SerializationStrategy<T> {
return (class2Serializer[value::class] ?: super.findPolymorphicSerializer(encoder, value)).cast()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ public abstract class AbstractPolymorphicSerializer<T : Any> internal constructo
public open fun findPolymorphicSerializer(
decoder: CompositeDecoder,
klassName: String
): KSerializer<out T> = decoder.context.getPolymorphic(baseClass, klassName)
?: throwSubtypeNotRegistered(klassName, baseClass)
): DeserializationStrategy<out T> = decoder.context.getPolymorphic(baseClass, klassName)
?: throwSubtypeNotRegistered(klassName, baseClass)


/**
Expand All @@ -92,7 +92,7 @@ public abstract class AbstractPolymorphicSerializer<T : Any> internal constructo
public open fun findPolymorphicSerializer(
encoder: Encoder,
value: T
): KSerializer<out T> =
): SerializationStrategy<T> =
encoder.context.getPolymorphic(baseClass, value) ?: throwSubtypeNotRegistered(value::class, baseClass)
}

Expand Down
8 changes: 8 additions & 0 deletions runtime/commonMain/src/kotlinx/serialization/internal/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,11 @@ internal fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object
@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
@PublishedApi
internal inline fun <T> KSerializer<*>.cast(): KSerializer<T> = this as KSerializer<T>

@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
@PublishedApi
internal inline fun <T> SerializationStrategy<*>.cast(): SerializationStrategy<T> = this as SerializationStrategy<T>

@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
@PublishedApi
internal inline fun <T> DeserializationStrategy<*>.cast(): DeserializationStrategy<T> = this as DeserializationStrategy<T>
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,18 @@ public abstract class JsonTransformingSerializer<T : Any>(

final override fun deserialize(decoder: Decoder): T {
val input = decoder.asJsonInput()
var element = input.decodeJson()
element = readTransform(element)
return input.json.fromJson(tSerializer, element)
val element = input.decodeJson()
return input.json.fromJson(tSerializer, readTransform(element))
}

/**
* Transformation which happens during [serialize] call.
* Transformation that happens during [serialize] call.
* Does nothing by default.
*/
protected open fun readTransform(element: JsonElement): JsonElement = element

/**
* Transformation which happens during [deserialize] call.
* Transformation that happens during [deserialize] call.
* Does nothing by default.
*/
protected open fun writeTransform(element: JsonElement): JsonElement = element
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ internal class ContextValidator(private val discriminator: String) : SerialModul
}
}
}

override fun <Base : Any> defaultPolymorphic(
baseClass: KClass<Base>,
defaultSerializerProvider: (className: String) -> DeserializationStrategy<out Base>?
) {
// Nothing here
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ internal inline fun <T> JsonOutput.encodePolymorphically(serializer: Serializati
}

private fun validateIfSealed(
serializer: KSerializer<*>,
actualSerializer: KSerializer<Any>,
serializer: SerializationStrategy<*>,
actualSerializer: SerializationStrategy<Any>,
classDiscriminator: String
) {
if (serializer !is SealedClassSerializer<*>) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package kotlinx.serialization.modules

import kotlinx.serialization.*
import kotlinx.serialization.internal.*
import kotlinx.serialization.json.*
import kotlin.reflect.*

/**
Expand All @@ -20,6 +21,7 @@ public class PolymorphicModuleBuilder<Base : Any> internal constructor(
private val baseSerializer: KSerializer<Base>? = null
) {
private val subclasses: MutableList<Pair<KClass<out Base>, KSerializer<out Base>>> = mutableListOf()
private var defaultSerializerProvider: ((String) -> DeserializationStrategy<out Base>?)? = null

/**
* Adds a [subclass] [serializer] to the resulting module under the initial [baseClass].
Expand Down Expand Up @@ -59,6 +61,22 @@ public class PolymorphicModuleBuilder<Base : Any> internal constructor(
@ImplicitReflectionSerializer
public inline fun <reified T : Base> subclass(): Unit = addSubclass(T::class, serializer())

/**
* Registers serializer provider that will be invoked if no polymorphic serializer is present.
* [defaultSerializerProvider] can be stateful and lookup a serializer for the missing type dynamically.
*
* Typically, if the class is not registered in advance, it is not possible to know the structure of the unknown
* type and have a precise serializer, so the default serializer has limited capabilities.
* To have a structural access to the unknown data, it is recommended to use [JsonTransformingSerializer]
* or [JsonParametricSerializer] classes.
*/
public fun default(defaultSerializerProvider: (className: String) -> DeserializationStrategy<out Base>?) {
require(this.defaultSerializerProvider == null) {
"Default serializer provider is already registered for class $baseClass: ${this.defaultSerializerProvider}"
}
this.defaultSerializerProvider = defaultSerializerProvider
}

/**
* @see addSubclass
*/
Expand All @@ -74,6 +92,10 @@ public class PolymorphicModuleBuilder<Base : Any> internal constructor(
serializer.cast()
)
}

if (defaultSerializerProvider != null) {
builder.registerDefaultPolymorphicSerializer(baseClass, defaultSerializerProvider!!, false)
}
}

/**
Expand Down
Loading

0 comments on commit a94f6f3

Please sign in to comment.