Skip to content
Merged
1 change: 1 addition & 0 deletions core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,7 @@ public final class kotlinx/serialization/modules/PolymorphicModuleBuilder {
public final fun default (Lkotlin/jvm/functions/Function1;)V
public final fun defaultDeserializer (Lkotlin/jvm/functions/Function1;)V
public final fun subclass (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V
public final fun subclassesOfSealed (Lkotlinx/serialization/KSerializer;)V
}

public abstract class kotlinx/serialization/modules/SerializersModule {
Expand Down
3 changes: 3 additions & 0 deletions core/api/kotlinx-serialization-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,11 @@ final class <#A: in kotlin/Any> kotlinx.serialization.modules/PolymorphicModuleB
constructor <init>(kotlin.reflect/KClass<#A>, kotlinx.serialization/KSerializer<#A>? = ...) // kotlinx.serialization.modules/PolymorphicModuleBuilder.<init>|<init>(kotlin.reflect.KClass<1:0>;kotlinx.serialization.KSerializer<1:0>?){}[0]

final fun <#A1: #A> subclass(kotlin.reflect/KClass<#A1>, kotlinx.serialization/KSerializer<#A1>) // kotlinx.serialization.modules/PolymorphicModuleBuilder.subclass|subclass(kotlin.reflect.KClass<0:0>;kotlinx.serialization.KSerializer<0:0>){0§<1:0>}[0]
final fun <#A1: #A> subclassesOfSealed(kotlinx.serialization/KSerializer<#A1>) // kotlinx.serialization.modules/PolymorphicModuleBuilder.subclassesOfSealed|subclassesOfSealed(kotlinx.serialization.KSerializer<0:0>){0§<1:0>}[0]
final fun buildTo(kotlinx.serialization.modules/SerializersModuleBuilder) // kotlinx.serialization.modules/PolymorphicModuleBuilder.buildTo|buildTo(kotlinx.serialization.modules.SerializersModuleBuilder){}[0]
final fun default(kotlin/Function1<kotlin/String?, kotlinx.serialization/DeserializationStrategy<#A>?>) // kotlinx.serialization.modules/PolymorphicModuleBuilder.default|default(kotlin.Function1<kotlin.String?,kotlinx.serialization.DeserializationStrategy<1:0>?>){}[0]
final fun defaultDeserializer(kotlin/Function1<kotlin/String?, kotlinx.serialization/DeserializationStrategy<#A>?>) // kotlinx.serialization.modules/PolymorphicModuleBuilder.defaultDeserializer|defaultDeserializer(kotlin.Function1<kotlin.String?,kotlinx.serialization.DeserializationStrategy<1:0>?>){}[0]
final inline fun <#A1: reified #A> subclassesOfSealed() // kotlinx.serialization.modules/PolymorphicModuleBuilder.subclassesOfSealed|subclassesOfSealed(){0§<1:0>}[0]
}

final class <#A: kotlin/Any, #B: #A?> kotlinx.serialization.internal/ReferenceArraySerializer : kotlinx.serialization.internal/CollectionLikeSerializer<#B, kotlin/Array<#B>, kotlin.collections/ArrayList<#B>> { // kotlinx.serialization.internal/ReferenceArraySerializer|null[0]
Expand Down Expand Up @@ -1171,6 +1173,7 @@ final inline fun (kotlinx.serialization.encoding/Encoder).kotlinx.serialization.
final inline fun (kotlinx.serialization.encoding/Encoder).kotlinx.serialization.encoding/encodeStructure(kotlinx.serialization.descriptors/SerialDescriptor, crossinline kotlin/Function1<kotlinx.serialization.encoding/CompositeEncoder, kotlin/Unit>) // kotlinx.serialization.encoding/encodeStructure|[email protected](kotlinx.serialization.descriptors.SerialDescriptor;kotlin.Function1<kotlinx.serialization.encoding.CompositeEncoder,kotlin.Unit>){}[0]
final inline fun <#A: kotlin/Any, #B: reified #A> (kotlinx.serialization.modules/PolymorphicModuleBuilder<#A>).kotlinx.serialization.modules/subclass(kotlin.reflect/KClass<#B>) // kotlinx.serialization.modules/subclass|[email protected]<0:0>(kotlin.reflect.KClass<0:1>){0§<kotlin.Any>;1§<0:0>}[0]
final inline fun <#A: kotlin/Any, #B: reified #A> (kotlinx.serialization.modules/PolymorphicModuleBuilder<#A>).kotlinx.serialization.modules/subclass(kotlinx.serialization/KSerializer<#B>) // kotlinx.serialization.modules/subclass|[email protected]<0:0>(kotlinx.serialization.KSerializer<0:1>){0§<kotlin.Any>;1§<0:0>}[0]
final inline fun <#A: kotlin/Any, #B: reified #A> (kotlinx.serialization.modules/PolymorphicModuleBuilder<#A>).kotlinx.serialization.modules/subclassesOfSealed() // kotlinx.serialization.modules/subclassesOfSealed|subclassesOfSealed@kotlinx.serialization.modules.PolymorphicModuleBuilder<0:0>(){0§<kotlin.Any>;1§<0:0>}[0]
final inline fun <#A: kotlin/Any> (kotlinx.serialization.modules/SerializersModuleBuilder).kotlinx.serialization.modules/polymorphic(kotlin.reflect/KClass<#A>, kotlinx.serialization/KSerializer<#A>? = ..., kotlin/Function1<kotlinx.serialization.modules/PolymorphicModuleBuilder<#A>, kotlin/Unit> = ...) // kotlinx.serialization.modules/polymorphic|polymorphic@kotlinx.serialization.modules.SerializersModuleBuilder(kotlin.reflect.KClass<0:0>;kotlinx.serialization.KSerializer<0:0>?;kotlin.Function1<kotlinx.serialization.modules.PolymorphicModuleBuilder<0:0>,kotlin.Unit>){0§<kotlin.Any>}[0]
final inline fun <#A: kotlin/Any?> (kotlinx.serialization.encoding/Decoder).kotlinx.serialization.encoding/decodeStructure(kotlinx.serialization.descriptors/SerialDescriptor, crossinline kotlin/Function1<kotlinx.serialization.encoding/CompositeDecoder, #A>): #A // kotlinx.serialization.encoding/decodeStructure|[email protected](kotlinx.serialization.descriptors.SerialDescriptor;kotlin.Function1<kotlinx.serialization.encoding.CompositeDecoder,0:0>){0§<kotlin.Any?>}[0]
final inline fun <#A: kotlin/Any?> (kotlinx.serialization.encoding/Encoder).kotlinx.serialization.encoding/encodeCollection(kotlinx.serialization.descriptors/SerialDescriptor, kotlin.collections/Collection<#A>, crossinline kotlin/Function3<kotlinx.serialization.encoding/CompositeEncoder, kotlin/Int, #A, kotlin/Unit>) // kotlinx.serialization.encoding/encodeCollection|[email protected](kotlinx.serialization.descriptors.SerialDescriptor;kotlin.collections.Collection<0:0>;kotlin.Function3<kotlinx.serialization.encoding.CompositeEncoder,kotlin.Int,0:0,kotlin.Unit>){0§<kotlin.Any?>}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public class SealedClassSerializer<T : Any>(
}
}

private val class2Serializer: Map<KClass<out T>, KSerializer<out T>>
internal val class2Serializer: Map<KClass<out T>, KSerializer<out T>>
private val serialName2Serializer: Map<String, KSerializer<out T>>

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package kotlinx.serialization.modules

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

Expand All @@ -23,6 +24,84 @@ public class PolymorphicModuleBuilder<in Base : Any> @PublishedApi internal cons
private var defaultSerializerProvider: ((Base) -> SerializationStrategy<Base>?)? = null
private var defaultDeserializerProvider: ((String?) -> DeserializationStrategy<Base>?)? = null


/**
* Registers the child serializers for the sealed type [T] in the resulting module under the [base class][Base].
* Please note that type `T` has to be sealed and have a standard serializer, if not a runtime error will be
* thrown at registration time. If one of [T]'s subclasses is a sealed serializable class on its own, its
* subclasses are registered recursively as well. If one of [T]'s subclasses is an open polymorphic class
* an [IllegalArgumentException] is thrown.
*
* This function is a convenience function for the version that receives a serializer.
*
* Example:
* ```
* interface Base
*
* @Serializable
* sealed interface Sub: Base
*
* @Serializable
* class Sub1: Sub
*
* serializersModule {
* polymorphic(Base::class) {
* subclassesOfSealed<Sub>()
* }
* }
* ```
*/
@ExperimentalSerializationApi
public inline fun <reified T : Base> subclassesOfSealed(): Unit =
subclassesOfSealed(serializer<T>())


/**
* Registers the child serializers for the sealed type [T] in the resulting module under the [base class][Base].
* Please note that type `T` has to be sealed and have a standard serializer, if not a runtime error will be
* thrown at registration time. If one of [T]'s subclasses is a sealed serializable class on its own, its
* subclasses are registered recursively as well. If one of [T]'s subclasses is an open polymorphic class
* an [IllegalArgumentException] is thrown.
*
* Example:
* ```kotlin
* interface Base
*
* @Serializable
* sealed interface Sub: Base
*
* @Serializable
* class Sub1: Sub
*
* serializersModule {
* polymorphic(Base::class) {
* subclassesOfSealed(Sub.serializer())
* }
* }
* ```
*
* Note that if Sub1 is itself open polymorphic this is an error.
*
*/
@ExperimentalSerializationApi
public fun <T: Base> subclassesOfSealed(serializer: KSerializer<T>) {
// Note that the parameter type is `KSerializer` as `SealedClassSerializer` is an internal type
// not available to users
require(serializer is SealedClassSerializer) {
"subclassesOfSealed only supports automatic adding of subclasses of sealed types with standard serializers."
}
for ((subsubclass, subserializer) in serializer.class2Serializer.entries) {
// This error would be caught by the Json format in its validation, but this is format specific
require (subserializer.descriptor.kind != PolymorphicKind.OPEN) {
"It is not possible to register subclasses (${serializer.descriptor.serialName}) of sealed types when those subclasses " +
"themselves are (open) polymorphic, as this would represent an incomplete hierarchy."
}
@Suppress("UNCHECKED_CAST")
// We don't know the type here, but it matches if correct in the sealed serializer.
subclass(subsubclass as KClass<T>, subserializer as KSerializer<T>)
}
}

/**
* Registers a [subclass] [serializer] in the resulting module under the [base class][Base].
*/
Expand Down Expand Up @@ -116,3 +195,10 @@ public inline fun <Base : Any, reified T : Base> PolymorphicModuleBuilder<Base>.
*/
public inline fun <Base : Any, reified T : Base> PolymorphicModuleBuilder<Base>.subclass(clazz: KClass<T>): Unit =
subclass(clazz, serializer())

/**
* Registers the child serializers for the sealed class [T] in the resulting module under the [base class][Base].
*/
@ExperimentalSerializationApi
public inline fun <Base : Any, reified T : Base> PolymorphicModuleBuilder<Base>.subclassesOfSealed(): Unit =
subclassesOfSealed(serializer<T>())
83 changes: 73 additions & 10 deletions docs/polymorphism.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ In this chapter we'll see how Kotlin Serialization deals with polymorphic class
* [Open polymorphism](#open-polymorphism)
* [Registered subclasses](#registered-subclasses)
* [Serializing interfaces](#serializing-interfaces)
* [Registering sealed children as subclasses](#registering-sealed-children-as-subclasses)
* [Property of an interface type](#property-of-an-interface-type)
* [Static parent type lookup for polymorphism](#static-parent-type-lookup-for-polymorphism)
* [Explicitly marking polymorphic class properties](#explicitly-marking-polymorphic-class-properties)
Expand Down Expand Up @@ -414,6 +415,68 @@ fun main() {

> Note: On Kotlin/Native, you should use `format.encodeToString(PolymorphicSerializer(Project::class), data))` instead due to limited reflection capabilities.

### Registering sealed children as subclasses
A sealed parent interface or class can be used to directly register all its children using `subclassesOfSealed`.
This will allow serializing the children using open polymorphism without the need to register each one individually.

If one of the type's subclasses is a sealed serializable class on its own, its subclasses are registered recursively
as well. However, if one of the type's subclasses is an open polymorphic class, an `IllegalArgumentException` is thrown.
In other words, all children/descendants must be either concrete or sealed.

<!--- TEST -->

<!--- INCLUDE
import kotlinx.serialization.modules.*
-->

```kotlin
interface Base

@Serializable
sealed interface Sub: Base

@Serializable
class Sub1(val data: String): Sub

val module1 = SerializersModule {
polymorphic(Base::class) {
subclassesOfSealed(Sub.serializer())
}
}

val format1 = Json { serializersModule = module1 }
```

Alternatively the convenience overload allows specifying the sealed type as type parameter.

```kotlin
val module2 = SerializersModule {
polymorphic(Base::class) {
subclassesOfSealed<Sub>()
}
}

val format2 = Json { serializersModule = module2 }
```

Now if we declare `data` with the type of `Base` we can simply call `format.encodeToString` as before.
```kotlin

fun main() {
val data: Base = Sub1("kotlin")
println(format1.encodeToString(data))
println(format2.encodeToString(data))
}
```

```text
{"type":"example.examplePoly11.Sub1","data":"kotlin"}
{"type":"example.examplePoly11.Sub1","data":"kotlin"}
```

> You can get the full code [here](../guide/example/example-poly-11.kt).


<!--- TEST LINES_START -->

### Property of an interface type
Expand Down Expand Up @@ -451,7 +514,7 @@ fun main() {
}
```

> You can get the full code [here](../guide/example/example-poly-11.kt).
> You can get the full code [here](../guide/example/example-poly-12.kt).

As long as we've registered the actual subtype of the interface that is being serialized in
the [SerializersModule] of our `format`, we get it working at runtime.
Expand Down Expand Up @@ -496,7 +559,7 @@ fun main() {
}
```

> You can get the full code [here](../guide/example/example-poly-12.kt).
> You can get the full code [here](../guide/example/example-poly-13.kt).

We get the exception.

Expand Down Expand Up @@ -544,7 +607,7 @@ fun main() {
}
```

> You can get the full code [here](../guide/example/example-poly-13.kt).
> You can get the full code [here](../guide/example/example-poly-14.kt).

However, `Any` is a class and it is not serializable:

Expand Down Expand Up @@ -586,7 +649,7 @@ fun main() {
}
```

> You can get the full code [here](../guide/example/example-poly-14.kt).
> You can get the full code [here](../guide/example/example-poly-15.kt).

With the explicit serializer it works as before.

Expand Down Expand Up @@ -639,7 +702,7 @@ fun main() {
}
```

> You can get the full code [here](../guide/example/example-poly-15.kt).
> You can get the full code [here](../guide/example/example-poly-16.kt).

<!--- TEST
{"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}
Expand Down Expand Up @@ -692,7 +755,7 @@ fun main() {
}
-->

> You can get the full code [here](../guide/example/example-poly-16.kt).
> You can get the full code [here](../guide/example/example-poly-17.kt).

<!--- TEST
{"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"},"any":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}
Expand Down Expand Up @@ -783,7 +846,7 @@ fun main() {

```

> You can get the full code [here](../guide/example/example-poly-17.kt).
> You can get the full code [here](../guide/example/example-poly-18.kt).

The JSON that is being produced is deeply polymorphic.

Expand Down Expand Up @@ -831,7 +894,7 @@ fun main() {
}
```

> You can get the full code [here](../guide/example/example-poly-18.kt).
> You can get the full code [here](../guide/example/example-poly-19.kt).

We get the following exception.

Expand Down Expand Up @@ -894,7 +957,7 @@ fun main() {
}
```

> You can get the full code [here](../guide/example/example-poly-19.kt).
> You can get the full code [here](../guide/example/example-poly-20.kt).

Notice, how `BasicProject` had also captured the specified type key in its `type` property.

Expand Down Expand Up @@ -998,7 +1061,7 @@ fun main() {
}
```

> You can get the full code [here](../guide/example/example-poly-20.kt)
> You can get the full code [here](../guide/example/example-poly-21.kt)

```text
{"type":"Cat","catType":"Tabby"}
Expand Down
1 change: 1 addition & 0 deletions docs/serialization-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Once the project is set up, we can start serializing some classes.
* <a name='open-polymorphism'></a>[Open polymorphism](polymorphism.md#open-polymorphism)
* <a name='registered-subclasses'></a>[Registered subclasses](polymorphism.md#registered-subclasses)
* <a name='serializing-interfaces'></a>[Serializing interfaces](polymorphism.md#serializing-interfaces)
* <a name='registering-sealed-children-as-subclasses'></a>[Registering sealed children as subclasses](polymorphism.md#registering-sealed-children-as-subclasses)
* <a name='property-of-an-interface-type'></a>[Property of an interface type](polymorphism.md#property-of-an-interface-type)
* <a name='static-parent-type-lookup-for-polymorphism'></a>[Static parent type lookup for polymorphism](polymorphism.md#static-parent-type-lookup-for-polymorphism)
* <a name='explicitly-marking-polymorphic-class-properties'></a>[Explicitly marking polymorphic class properties](polymorphism.md#explicitly-marking-polymorphic-class-properties)
Expand Down
Loading