Skip to content

Commit fc62da3

Browse files
authored
'subclassesOfSealed' function for automatically registering sealed serializers children in polymorphic modules (#2201)
Allows for the automatic registration of sealed children of a class for polymorphic serialization of a shared (non-sealed) type. Fixes #2199
1 parent 70bc1b6 commit fc62da3

19 files changed

+493
-178
lines changed

core/api/kotlinx-serialization-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,7 @@ public final class kotlinx/serialization/modules/PolymorphicModuleBuilder {
13361336
public final fun default (Lkotlin/jvm/functions/Function1;)V
13371337
public final fun defaultDeserializer (Lkotlin/jvm/functions/Function1;)V
13381338
public final fun subclass (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;)V
1339+
public final fun subclassesOfSealed (Lkotlinx/serialization/KSerializer;)V
13391340
}
13401341

13411342
public abstract class kotlinx/serialization/modules/SerializersModule {

core/api/kotlinx-serialization-core.klib.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,9 +527,11 @@ final class <#A: in kotlin/Any> kotlinx.serialization.modules/PolymorphicModuleB
527527
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]
528528

529529
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]
530+
final fun <#A1: #A> subclassesOfSealed(kotlinx.serialization/KSerializer<#A1>) // kotlinx.serialization.modules/PolymorphicModuleBuilder.subclassesOfSealed|subclassesOfSealed(kotlinx.serialization.KSerializer<0:0>){0§<1:0>}[0]
530531
final fun buildTo(kotlinx.serialization.modules/SerializersModuleBuilder) // kotlinx.serialization.modules/PolymorphicModuleBuilder.buildTo|buildTo(kotlinx.serialization.modules.SerializersModuleBuilder){}[0]
531532
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]
532533
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]
534+
final inline fun <#A1: reified #A> subclassesOfSealed() // kotlinx.serialization.modules/PolymorphicModuleBuilder.subclassesOfSealed|subclassesOfSealed(){0§<1:0>}[0]
533535
}
534536

535537
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]
@@ -1171,6 +1173,7 @@ final inline fun (kotlinx.serialization.encoding/Encoder).kotlinx.serialization.
11711173
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]
11721174
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]
11731175
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]
1176+
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]
11741177
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]
11751178
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]
11761179
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]

core/commonMain/src/kotlinx/serialization/SealedSerializer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public class SealedClassSerializer<T : Any>(
115115
}
116116
}
117117

118-
private val class2Serializer: Map<KClass<out T>, KSerializer<out T>>
118+
internal val class2Serializer: Map<KClass<out T>, KSerializer<out T>>
119119
private val serialName2Serializer: Map<String, KSerializer<out T>>
120120

121121
init {

core/commonMain/src/kotlinx/serialization/modules/PolymorphicModuleBuilder.kt

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package kotlinx.serialization.modules
66

77
import kotlinx.serialization.*
8+
import kotlinx.serialization.descriptors.*
89
import kotlinx.serialization.internal.*
910
import kotlin.reflect.*
1011

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

27+
28+
/**
29+
* Registers the child serializers for the sealed type [T] in the resulting module under the [base class][Base].
30+
* Please note that type `T` has to be sealed and have a standard serializer, if not a runtime error will be
31+
* thrown at registration time. If one of [T]'s subclasses is a sealed serializable class on its own, its
32+
* subclasses are registered recursively as well. If one of [T]'s subclasses is an open polymorphic class
33+
* an [IllegalArgumentException] is thrown.
34+
*
35+
* This function is a convenience function for the version that receives a serializer.
36+
*
37+
* Example:
38+
* ```
39+
* interface Base
40+
*
41+
* @Serializable
42+
* sealed interface Sub: Base
43+
*
44+
* @Serializable
45+
* class Sub1: Sub
46+
*
47+
* serializersModule {
48+
* polymorphic(Base::class) {
49+
* subclassesOfSealed<Sub>()
50+
* }
51+
* }
52+
* ```
53+
*/
54+
@ExperimentalSerializationApi
55+
public inline fun <reified T : Base> subclassesOfSealed(): Unit =
56+
subclassesOfSealed(serializer<T>())
57+
58+
59+
/**
60+
* Registers the child serializers for the sealed type [T] in the resulting module under the [base class][Base].
61+
* Please note that type `T` has to be sealed and have a standard serializer, if not a runtime error will be
62+
* thrown at registration time. If one of [T]'s subclasses is a sealed serializable class on its own, its
63+
* subclasses are registered recursively as well. If one of [T]'s subclasses is an open polymorphic class
64+
* an [IllegalArgumentException] is thrown.
65+
*
66+
* Example:
67+
* ```kotlin
68+
* interface Base
69+
*
70+
* @Serializable
71+
* sealed interface Sub: Base
72+
*
73+
* @Serializable
74+
* class Sub1: Sub
75+
*
76+
* serializersModule {
77+
* polymorphic(Base::class) {
78+
* subclassesOfSealed(Sub.serializer())
79+
* }
80+
* }
81+
* ```
82+
*
83+
* Note that if Sub1 is itself open polymorphic this is an error.
84+
*
85+
*/
86+
@ExperimentalSerializationApi
87+
public fun <T: Base> subclassesOfSealed(serializer: KSerializer<T>) {
88+
// Note that the parameter type is `KSerializer` as `SealedClassSerializer` is an internal type
89+
// not available to users
90+
require(serializer is SealedClassSerializer) {
91+
"subclassesOfSealed only supports automatic adding of subclasses of sealed types with standard serializers."
92+
}
93+
for ((subsubclass, subserializer) in serializer.class2Serializer.entries) {
94+
// This error would be caught by the Json format in its validation, but this is format specific
95+
require (subserializer.descriptor.kind != PolymorphicKind.OPEN) {
96+
"It is not possible to register subclasses (${serializer.descriptor.serialName}) of sealed types when those subclasses " +
97+
"themselves are (open) polymorphic, as this would represent an incomplete hierarchy."
98+
}
99+
@Suppress("UNCHECKED_CAST")
100+
// We don't know the type here, but it matches if correct in the sealed serializer.
101+
subclass(subsubclass as KClass<T>, subserializer as KSerializer<T>)
102+
}
103+
}
104+
26105
/**
27106
* Registers a [subclass] [serializer] in the resulting module under the [base class][Base].
28107
*/
@@ -116,3 +195,10 @@ public inline fun <Base : Any, reified T : Base> PolymorphicModuleBuilder<Base>.
116195
*/
117196
public inline fun <Base : Any, reified T : Base> PolymorphicModuleBuilder<Base>.subclass(clazz: KClass<T>): Unit =
118197
subclass(clazz, serializer())
198+
199+
/**
200+
* Registers the child serializers for the sealed class [T] in the resulting module under the [base class][Base].
201+
*/
202+
@ExperimentalSerializationApi
203+
public inline fun <Base : Any, reified T : Base> PolymorphicModuleBuilder<Base>.subclassesOfSealed(): Unit =
204+
subclassesOfSealed(serializer<T>())

docs/polymorphism.md

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ In this chapter we'll see how Kotlin Serialization deals with polymorphic class
1919
* [Open polymorphism](#open-polymorphism)
2020
* [Registered subclasses](#registered-subclasses)
2121
* [Serializing interfaces](#serializing-interfaces)
22+
* [Registering sealed children as subclasses](#registering-sealed-children-as-subclasses)
2223
* [Property of an interface type](#property-of-an-interface-type)
2324
* [Static parent type lookup for polymorphism](#static-parent-type-lookup-for-polymorphism)
2425
* [Explicitly marking polymorphic class properties](#explicitly-marking-polymorphic-class-properties)
@@ -414,6 +415,68 @@ fun main() {
414415

415416
> Note: On Kotlin/Native, you should use `format.encodeToString(PolymorphicSerializer(Project::class), data))` instead due to limited reflection capabilities.
416417
418+
### Registering sealed children as subclasses
419+
A sealed parent interface or class can be used to directly register all its children using `subclassesOfSealed`.
420+
This will allow serializing the children using open polymorphism without the need to register each one individually.
421+
422+
If one of the type's subclasses is a sealed serializable class on its own, its subclasses are registered recursively
423+
as well. However, if one of the type's subclasses is an open polymorphic class, an `IllegalArgumentException` is thrown.
424+
In other words, all children/descendants must be either concrete or sealed.
425+
426+
<!--- TEST -->
427+
428+
<!--- INCLUDE
429+
import kotlinx.serialization.modules.*
430+
-->
431+
432+
```kotlin
433+
interface Base
434+
435+
@Serializable
436+
sealed interface Sub: Base
437+
438+
@Serializable
439+
class Sub1(val data: String): Sub
440+
441+
val module1 = SerializersModule {
442+
polymorphic(Base::class) {
443+
subclassesOfSealed(Sub.serializer())
444+
}
445+
}
446+
447+
val format1 = Json { serializersModule = module1 }
448+
```
449+
450+
Alternatively the convenience overload allows specifying the sealed type as type parameter.
451+
452+
```kotlin
453+
val module2 = SerializersModule {
454+
polymorphic(Base::class) {
455+
subclassesOfSealed<Sub>()
456+
}
457+
}
458+
459+
val format2 = Json { serializersModule = module2 }
460+
```
461+
462+
Now if we declare `data` with the type of `Base` we can simply call `format.encodeToString` as before.
463+
```kotlin
464+
465+
fun main() {
466+
val data: Base = Sub1("kotlin")
467+
println(format1.encodeToString(data))
468+
println(format2.encodeToString(data))
469+
}
470+
```
471+
472+
```text
473+
{"type":"example.examplePoly11.Sub1","data":"kotlin"}
474+
{"type":"example.examplePoly11.Sub1","data":"kotlin"}
475+
```
476+
477+
> You can get the full code [here](../guide/example/example-poly-11.kt).
478+
479+
417480
<!--- TEST LINES_START -->
418481

419482
### Property of an interface type
@@ -451,7 +514,7 @@ fun main() {
451514
}
452515
```
453516

454-
> You can get the full code [here](../guide/example/example-poly-11.kt).
517+
> You can get the full code [here](../guide/example/example-poly-12.kt).
455518
456519
As long as we've registered the actual subtype of the interface that is being serialized in
457520
the [SerializersModule] of our `format`, we get it working at runtime.
@@ -496,7 +559,7 @@ fun main() {
496559
}
497560
```
498561

499-
> You can get the full code [here](../guide/example/example-poly-12.kt).
562+
> You can get the full code [here](../guide/example/example-poly-13.kt).
500563
501564
We get the exception.
502565

@@ -544,7 +607,7 @@ fun main() {
544607
}
545608
```
546609

547-
> You can get the full code [here](../guide/example/example-poly-13.kt).
610+
> You can get the full code [here](../guide/example/example-poly-14.kt).
548611
549612
However, `Any` is a class and it is not serializable:
550613

@@ -586,7 +649,7 @@ fun main() {
586649
}
587650
```
588651

589-
> You can get the full code [here](../guide/example/example-poly-14.kt).
652+
> You can get the full code [here](../guide/example/example-poly-15.kt).
590653
591654
With the explicit serializer it works as before.
592655

@@ -639,7 +702,7 @@ fun main() {
639702
}
640703
```
641704

642-
> You can get the full code [here](../guide/example/example-poly-15.kt).
705+
> You can get the full code [here](../guide/example/example-poly-16.kt).
643706
644707
<!--- TEST
645708
{"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}
@@ -692,7 +755,7 @@ fun main() {
692755
}
693756
-->
694757

695-
> You can get the full code [here](../guide/example/example-poly-16.kt).
758+
> You can get the full code [here](../guide/example/example-poly-17.kt).
696759
697760
<!--- TEST
698761
{"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"},"any":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}
@@ -783,7 +846,7 @@ fun main() {
783846

784847
```
785848

786-
> You can get the full code [here](../guide/example/example-poly-17.kt).
849+
> You can get the full code [here](../guide/example/example-poly-18.kt).
787850
788851
The JSON that is being produced is deeply polymorphic.
789852

@@ -831,7 +894,7 @@ fun main() {
831894
}
832895
```
833896

834-
> You can get the full code [here](../guide/example/example-poly-18.kt).
897+
> You can get the full code [here](../guide/example/example-poly-19.kt).
835898
836899
We get the following exception.
837900

@@ -894,7 +957,7 @@ fun main() {
894957
}
895958
```
896959

897-
> You can get the full code [here](../guide/example/example-poly-19.kt).
960+
> You can get the full code [here](../guide/example/example-poly-20.kt).
898961
899962
Notice, how `BasicProject` had also captured the specified type key in its `type` property.
900963

@@ -998,7 +1061,7 @@ fun main() {
9981061
}
9991062
```
10001063

1001-
> You can get the full code [here](../guide/example/example-poly-20.kt)
1064+
> You can get the full code [here](../guide/example/example-poly-21.kt)
10021065
10031066
```text
10041067
{"type":"Cat","catType":"Tabby"}

docs/serialization-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ Once the project is set up, we can start serializing some classes.
9797
* <a name='open-polymorphism'></a>[Open polymorphism](polymorphism.md#open-polymorphism)
9898
* <a name='registered-subclasses'></a>[Registered subclasses](polymorphism.md#registered-subclasses)
9999
* <a name='serializing-interfaces'></a>[Serializing interfaces](polymorphism.md#serializing-interfaces)
100+
* <a name='registering-sealed-children-as-subclasses'></a>[Registering sealed children as subclasses](polymorphism.md#registering-sealed-children-as-subclasses)
100101
* <a name='property-of-an-interface-type'></a>[Property of an interface type](polymorphism.md#property-of-an-interface-type)
101102
* <a name='static-parent-type-lookup-for-polymorphism'></a>[Static parent type lookup for polymorphism](polymorphism.md#static-parent-type-lookup-for-polymorphism)
102103
* <a name='explicitly-marking-polymorphic-class-properties'></a>[Explicitly marking polymorphic class properties](polymorphism.md#explicitly-marking-polymorphic-class-properties)

0 commit comments

Comments
 (0)