Skip to content

Commit 566d9aa

Browse files
authored
Merge pull request #271 from iruizmar/serialize_sealed_classes
Serialize sealed classes
2 parents d413cb6 + 8576cf3 commit 566d9aa

File tree

12 files changed

+197
-31
lines changed

12 files changed

+197
-31
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,33 @@ data class Post(
102102

103103
```
104104

105+
<h4>Polymorphic serialization (sealed classes)</h4>
106+
107+
This sdk will handle polymorphic serialization automatically if you have a sealed class and its children marked as `Serializable`. It will include a `type` property that will be used to discriminate which child class is the serialized.
108+
109+
You can change this `type` property by using the `@FirebaseClassDiscrminator` annotation in the parent sealed class:
110+
111+
```kotlin
112+
@Serializable
113+
@FirebaseClassDiscriminator("class")
114+
sealed class Parent {
115+
@Serializable
116+
@SerialName("child")
117+
data class Child(
118+
val property: Boolean
119+
) : Parent
120+
}
121+
```
122+
123+
In combination with a `SerialName` specified for the child class, you have full control over the serialized data. In this case it will be:
124+
125+
```json
126+
{
127+
"class": "child",
128+
"property": true
129+
}
130+
```
131+
105132
<h3><a href="https://kotlinlang.org/docs/reference/functions.html#default-arguments">Default arguments</a></h3>
106133

107134
To reduce boilerplate, default arguments are used in the places where the Firebase Android SDK employs the builder pattern:

firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_decoders.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
package dev.gitlive.firebase
66

77
import kotlinx.serialization.encoding.CompositeDecoder
8-
import kotlinx.serialization.KSerializer
9-
import kotlinx.serialization.SerializationException
8+
import kotlinx.serialization.descriptors.PolymorphicKind
109
import kotlinx.serialization.descriptors.SerialDescriptor
1110
import kotlinx.serialization.descriptors.StructureKind
1211

13-
actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decodeDouble: (value: Any?) -> Double?): CompositeDecoder = when(descriptor.kind as StructureKind) {
14-
StructureKind.CLASS, StructureKind.OBJECT -> (value as Map<*, *>).let { map ->
12+
actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decodeDouble: (value: Any?) -> Double?): CompositeDecoder = when(descriptor.kind) {
13+
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> (value as Map<*, *>).let { map ->
1514
FirebaseClassDecoder(decodeDouble, map.size, { map.containsKey(it) }) { desc, index -> map[desc.getElementName(index)] }
1615
}
1716
StructureKind.LIST -> (value as List<*>).let {
@@ -20,4 +19,8 @@ actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decode
2019
StructureKind.MAP -> (value as Map<*, *>).entries.toList().let {
2120
FirebaseCompositeDecoder(decodeDouble, it.size) { _, index -> it[index/2].run { if(index % 2 == 0) key else value } }
2221
}
23-
}
22+
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
23+
}
24+
25+
actual fun getPolymorphicType(value: Any?, discriminator: String): String =
26+
(value as Map<*,*>)[discriminator] as String

firebase-common/src/androidMain/kotlin/dev/gitlive/firebase/_encoders.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@
44

55
package dev.gitlive.firebase
66

7-
import kotlinx.serialization.encoding.CompositeEncoder
87
import kotlinx.serialization.descriptors.SerialDescriptor
98
import kotlinx.serialization.descriptors.StructureKind
109
import kotlin.collections.set
1110

12-
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind as StructureKind) {
11+
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
1312
StructureKind.LIST -> mutableListOf<Any?>()
1413
.also { value = it }
1514
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
1615
StructureKind.MAP -> mutableListOf<Any?>()
1716
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
1817
StructureKind.CLASS, StructureKind.OBJECT -> mutableMapOf<Any?, Any?>()
1918
.also { value = it }
20-
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
19+
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity,
20+
setPolymorphicType = { discriminator, type ->
21+
it[discriminator] = type
22+
},
23+
set = { _, index, value -> it[descriptor.getElementName(index)] = value }
24+
) }
25+
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
2126
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package dev.gitlive.firebase
2+
3+
import kotlinx.serialization.InheritableSerialInfo
4+
5+
@InheritableSerialInfo
6+
@Target(AnnotationTarget.CLASS)
7+
annotation class FirebaseClassDiscriminator(val discriminator: String)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package dev.gitlive.firebase
2+
3+
import kotlinx.serialization.DeserializationStrategy
4+
import kotlinx.serialization.SerializationStrategy
5+
import kotlinx.serialization.descriptors.SerialDescriptor
6+
import kotlinx.serialization.findPolymorphicSerializer
7+
import kotlinx.serialization.internal.AbstractPolymorphicSerializer
8+
9+
/*
10+
* This code was inspired on polymorphic json serialization of kotlinx.serialization.
11+
* See https://github.com/Kotlin/kotlinx.serialization/blob/master/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt
12+
*/
13+
@Suppress("UNCHECKED_CAST")
14+
internal fun <T> FirebaseEncoder.encodePolymorphically(
15+
serializer: SerializationStrategy<T>,
16+
value: T,
17+
ifPolymorphic: (String) -> Unit
18+
) {
19+
if (serializer !is AbstractPolymorphicSerializer<*>) {
20+
serializer.serialize(this, value)
21+
return
22+
}
23+
val casted = serializer as AbstractPolymorphicSerializer<Any>
24+
val baseClassDiscriminator = serializer.descriptor.classDiscriminator()
25+
val actualSerializer = casted.findPolymorphicSerializer(this, value as Any)
26+
ifPolymorphic(baseClassDiscriminator)
27+
actualSerializer.serialize(this, value)
28+
}
29+
30+
@Suppress("UNCHECKED_CAST")
31+
internal fun <T> FirebaseDecoder.decodeSerializableValuePolymorphic(
32+
value: Any?,
33+
decodeDouble: (value: Any?) -> Double?,
34+
deserializer: DeserializationStrategy<T>,
35+
): T {
36+
if (deserializer !is AbstractPolymorphicSerializer<*>) {
37+
return deserializer.deserialize(this)
38+
}
39+
40+
val casted = deserializer as AbstractPolymorphicSerializer<Any>
41+
val discriminator = deserializer.descriptor.classDiscriminator()
42+
val type = getPolymorphicType(value, discriminator)
43+
val actualDeserializer = casted.findPolymorphicSerializerOrNull(
44+
structureDecoder(deserializer.descriptor, decodeDouble),
45+
type
46+
) as DeserializationStrategy<T>
47+
return actualDeserializer.deserialize(this)
48+
}
49+
50+
internal fun SerialDescriptor.classDiscriminator(): String {
51+
// Plain loop is faster than allocation of Sequence or ArrayList
52+
// We can rely on the fact that only one FirebaseClassDiscriminator is present —
53+
// compiler plugin checked that.
54+
for (annotation in annotations) {
55+
if (annotation is FirebaseClassDiscriminator) return annotation.discriminator
56+
}
57+
return "type"
58+
}
59+

firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/decoders.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ fun <T> decode(strategy: DeserializationStrategy<T>, value: Any?, decodeDouble:
2525
require(value != null || strategy.descriptor.isNullable) { "Value was null for non-nullable type ${strategy.descriptor.serialName}" }
2626
return FirebaseDecoder(value, decodeDouble).decodeSerializableValue(strategy)
2727
}
28-
2928
expect fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decodeDouble: (value: Any?) -> Double?): CompositeDecoder
29+
expect fun getPolymorphicType(value: Any?, discriminator: String): String
3030

3131
class FirebaseDecoder(internal val value: Any?, private val decodeDouble: (value: Any?) -> Double?) : Decoder {
3232

@@ -59,8 +59,11 @@ class FirebaseDecoder(internal val value: Any?, private val decodeDouble: (value
5959

6060
override fun decodeNull() = decodeNull(value)
6161

62-
@ExperimentalSerializationApi
6362
override fun decodeInline(inlineDescriptor: SerialDescriptor) = FirebaseDecoder(value, decodeDouble)
63+
64+
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
65+
return decodeSerializableValuePolymorphic(value, decodeDouble, deserializer)
66+
}
6467
}
6568

6669
class FirebaseClassDecoder(
@@ -80,7 +83,7 @@ class FirebaseClassDecoder(
8083
?: DECODE_DONE
8184
}
8285

83-
open class FirebaseCompositeDecoder constructor(
86+
open class FirebaseCompositeDecoder(
8487
private val decodeDouble: (value: Any?) -> Double?,
8588
private val size: Int,
8689
private val get: (descriptor: SerialDescriptor, index: Int) -> Any?
@@ -134,6 +137,7 @@ open class FirebaseCompositeDecoder constructor(
134137
@ExperimentalSerializationApi
135138
override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder =
136139
FirebaseDecoder(get(descriptor, index), decodeDouble)
140+
137141
}
138142

139143
private fun decodeString(value: Any?) = value.toString()

firebase-common/src/commonMain/kotlin/dev/gitlive/firebase/encoders.kt

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@ inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean, pos
1616
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply { encodeSerializableValue(it.firebaseSerializer(), it) }.value
1717
}
1818

19-
expect fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder
19+
expect fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder
2020

2121
class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positiveInfinity: Any) : TimestampEncoder(positiveInfinity), Encoder {
2222

2323
var value: Any? = null
2424

2525
override val serializersModule = EmptySerializersModule
26-
override fun beginStructure(descriptor: SerialDescriptor) = structureEncoder(descriptor)
26+
private var polymorphicDiscriminator: String? = null
27+
28+
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
29+
val encoder = structureEncoder(descriptor)
30+
if (polymorphicDiscriminator != null) {
31+
encoder.encodePolymorphicClassDiscriminator(polymorphicDiscriminator!!, descriptor.serialName)
32+
polymorphicDiscriminator = null
33+
}
34+
return encoder
35+
}
2736

2837
override fun encodeBoolean(value: Boolean) {
2938
this.value = value
@@ -73,9 +82,14 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive
7382
this.value = value
7483
}
7584

76-
@ExperimentalSerializationApi
7785
override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder =
7886
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
87+
88+
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
89+
encodePolymorphically(serializer, value) {
90+
polymorphicDiscriminator = it
91+
}
92+
}
7993
}
8094

8195
abstract class TimestampEncoder(internal val positiveInfinity: Any) {
@@ -89,7 +103,8 @@ open class FirebaseCompositeEncoder constructor(
89103
private val shouldEncodeElementDefault: Boolean,
90104
positiveInfinity: Any,
91105
private val end: () -> Unit = {},
92-
private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit
106+
private val setPolymorphicType: (String, String) -> Unit = { _, _ -> },
107+
private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit,
93108
): TimestampEncoder(positiveInfinity), CompositeEncoder {
94109

95110
override val serializersModule = EmptySerializersModule
@@ -153,6 +168,9 @@ open class FirebaseCompositeEncoder constructor(
153168
@ExperimentalSerializationApi
154169
override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder =
155170
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
156-
}
157171

172+
fun encodePolymorphicClassDiscriminator(discriminator: String, type: String) {
173+
setPolymorphicType(discriminator, type)
174+
}
175+
}
158176

firebase-common/src/commonTest/kotlin/dev/gitlive/firebase/EncodersTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package dev.gitlive.firebase
66

7+
import kotlinx.serialization.SerialName
78
import kotlinx.serialization.Serializable
89
import kotlinx.serialization.builtins.ListSerializer
910
import kotlin.test.Test
@@ -17,6 +18,13 @@ expect fun nativeAssertEquals(expected: Any?, actual: Any?): Unit
1718
@Serializable
1819
data class TestData(val map: Map<String, String>, val bool: Boolean = false, val nullableBool: Boolean? = null)
1920

21+
@Serializable
22+
sealed class TestSealed {
23+
@Serializable
24+
@SerialName("child")
25+
data class ChildClass(val map: Map<String, String>, val bool: Boolean = false): TestSealed()
26+
}
27+
2028
class EncodersTest {
2129
@Test
2230
fun encodeMap() {
@@ -37,6 +45,12 @@ class EncodersTest {
3745
nativeAssertEquals(nativeMapOf("map" to nativeMapOf("key" to "value"), "bool" to true, "nullableBool" to true), encoded)
3846
}
3947

48+
@Test
49+
fun encodeSealedClass() {
50+
val encoded = encode<TestSealed>(TestSealed.serializer(), TestSealed.ChildClass(mapOf("key" to "value"), true), shouldEncodeElementDefault = true)
51+
nativeAssertEquals(nativeMapOf("type" to "child", "map" to nativeMapOf("key" to "value"), "bool" to true), encoded)
52+
}
53+
4054
@Test
4155
fun decodeObject() {
4256
val decoded = decode<TestData>(TestData.serializer(), nativeMapOf("map" to nativeMapOf("key" to "value")))
@@ -54,4 +68,10 @@ class EncodersTest {
5468
val decoded = decode(TestData.serializer(), nativeMapOf("map" to mapOf("key" to "value"), "nullableBool" to null))
5569
assertNull(decoded.nullableBool)
5670
}
71+
72+
@Test
73+
fun decodeSealedClass() {
74+
val decoded = decode(TestSealed.serializer(), nativeMapOf("type" to "child", "map" to nativeMapOf("key" to "value"), "bool" to true))
75+
assertEquals(TestSealed.ChildClass(mapOf("key" to "value"), true), decoded)
76+
}
5777
}

firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_decoders.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
package dev.gitlive.firebase
66

77
import kotlinx.serialization.encoding.CompositeDecoder
8-
import kotlinx.serialization.KSerializer
9-
import kotlinx.serialization.SerializationException
8+
import kotlinx.serialization.descriptors.PolymorphicKind
109
import kotlinx.serialization.descriptors.SerialDescriptor
1110
import kotlinx.serialization.descriptors.StructureKind
1211

13-
actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decodeDouble: (value: Any?) -> Double?): CompositeDecoder = when(descriptor.kind as StructureKind) {
14-
StructureKind.CLASS, StructureKind.OBJECT -> (value as Map<*, *>).let { map ->
12+
actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decodeDouble: (value: Any?) -> Double?): CompositeDecoder = when(descriptor.kind) {
13+
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> (value as Map<*, *>).let { map ->
1514
FirebaseClassDecoder(decodeDouble, map.size, { map.containsKey(it) }) { desc, index -> map[desc.getElementName(index)] }
1615
}
1716
StructureKind.LIST -> (value as List<*>).let {
@@ -20,4 +19,8 @@ actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decode
2019
StructureKind.MAP -> (value as Map<*, *>).entries.toList().let {
2120
FirebaseCompositeDecoder(decodeDouble, it.size) { _, index -> it[index/2].run { if(index % 2 == 0) key else value } }
2221
}
23-
}
22+
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
23+
}
24+
25+
actual fun getPolymorphicType(value: Any?, discriminator: String): String =
26+
(value as Map<*,*>)[discriminator] as String

firebase-common/src/iosMain/kotlin/dev/gitlive/firebase/_encoders.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,25 @@
44

55
package dev.gitlive.firebase
66

7+
import kotlinx.serialization.descriptors.PolymorphicKind
78
import kotlinx.serialization.encoding.CompositeEncoder
89
import kotlinx.serialization.descriptors.SerialDescriptor
910
import kotlinx.serialization.descriptors.StructureKind
1011
import kotlin.collections.set
1112

12-
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind as StructureKind) {
13+
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
1314
StructureKind.LIST -> mutableListOf<Any?>()
1415
.also { value = it }
1516
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
1617
StructureKind.MAP -> mutableListOf<Any?>()
1718
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
18-
StructureKind.CLASS, StructureKind.OBJECT -> mutableMapOf<Any?, Any?>()
19+
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> mutableMapOf<Any?, Any?>()
1920
.also { value = it }
20-
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
21+
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity,
22+
setPolymorphicType = { discriminator, type ->
23+
it[discriminator] = type
24+
},
25+
set = { _, index, value -> it[descriptor.getElementName(index)] = value }
26+
) }
27+
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
2128
}

0 commit comments

Comments
 (0)