Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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|encodeStructure@kotlinx.serialization.encoding.Encoder(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|subclass@kotlinx.serialization.modules.PolymorphicModuleBuilder<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|subclass@kotlinx.serialization.modules.PolymorphicModuleBuilder<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|decodeStructure@kotlinx.serialization.encoding.Decoder(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|encodeCollection@kotlinx.serialization.encoding.Encoder(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