Skip to content

Commit

Permalink
Merge pull request #271 from iruizmar/serialize_sealed_classes
Browse files Browse the repository at this point in the history
Serialize sealed classes
  • Loading branch information
nbransby authored Apr 6, 2022
2 parents d413cb6 + 8576cf3 commit 566d9aa
Show file tree
Hide file tree
Showing 12 changed files with 197 additions and 31 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,33 @@ data class Post(

```

<h4>Polymorphic serialization (sealed classes)</h4>

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.

You can change this `type` property by using the `@FirebaseClassDiscrminator` annotation in the parent sealed class:

```kotlin
@Serializable
@FirebaseClassDiscriminator("class")
sealed class Parent {
@Serializable
@SerialName("child")
data class Child(
val property: Boolean
) : Parent
}
```

In combination with a `SerialName` specified for the child class, you have full control over the serialized data. In this case it will be:

```json
{
"class": "child",
"property": true
}
```

<h3><a href="https://kotlinlang.org/docs/reference/functions.html#default-arguments">Default arguments</a></h3>

To reduce boilerplate, default arguments are used in the places where the Firebase Android SDK employs the builder pattern:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
package dev.gitlive.firebase

import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind

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

actual fun getPolymorphicType(value: Any?, discriminator: String): String =
(value as Map<*,*>)[discriminator] as String
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@

package dev.gitlive.firebase

import kotlinx.serialization.encoding.CompositeEncoder
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlin.collections.set

actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind as StructureKind) {
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
StructureKind.LIST -> mutableListOf<Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
StructureKind.MAP -> mutableListOf<Any?>()
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
StructureKind.CLASS, StructureKind.OBJECT -> mutableMapOf<Any?, Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity,
setPolymorphicType = { discriminator, type ->
it[discriminator] = type
},
set = { _, index, value -> it[descriptor.getElementName(index)] = value }
) }
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.gitlive.firebase

import kotlinx.serialization.InheritableSerialInfo

@InheritableSerialInfo
@Target(AnnotationTarget.CLASS)
annotation class FirebaseClassDiscriminator(val discriminator: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package dev.gitlive.firebase

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.findPolymorphicSerializer
import kotlinx.serialization.internal.AbstractPolymorphicSerializer

/*
* This code was inspired on polymorphic json serialization of kotlinx.serialization.
* See https://github.com/Kotlin/kotlinx.serialization/blob/master/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt
*/
@Suppress("UNCHECKED_CAST")
internal fun <T> FirebaseEncoder.encodePolymorphically(
serializer: SerializationStrategy<T>,
value: T,
ifPolymorphic: (String) -> Unit
) {
if (serializer !is AbstractPolymorphicSerializer<*>) {
serializer.serialize(this, value)
return
}
val casted = serializer as AbstractPolymorphicSerializer<Any>
val baseClassDiscriminator = serializer.descriptor.classDiscriminator()
val actualSerializer = casted.findPolymorphicSerializer(this, value as Any)
ifPolymorphic(baseClassDiscriminator)
actualSerializer.serialize(this, value)
}

@Suppress("UNCHECKED_CAST")
internal fun <T> FirebaseDecoder.decodeSerializableValuePolymorphic(
value: Any?,
decodeDouble: (value: Any?) -> Double?,
deserializer: DeserializationStrategy<T>,
): T {
if (deserializer !is AbstractPolymorphicSerializer<*>) {
return deserializer.deserialize(this)
}

val casted = deserializer as AbstractPolymorphicSerializer<Any>
val discriminator = deserializer.descriptor.classDiscriminator()
val type = getPolymorphicType(value, discriminator)
val actualDeserializer = casted.findPolymorphicSerializerOrNull(
structureDecoder(deserializer.descriptor, decodeDouble),
type
) as DeserializationStrategy<T>
return actualDeserializer.deserialize(this)
}

internal fun SerialDescriptor.classDiscriminator(): String {
// Plain loop is faster than allocation of Sequence or ArrayList
// We can rely on the fact that only one FirebaseClassDiscriminator is present —
// compiler plugin checked that.
for (annotation in annotations) {
if (annotation is FirebaseClassDiscriminator) return annotation.discriminator
}
return "type"
}

Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ fun <T> decode(strategy: DeserializationStrategy<T>, value: Any?, decodeDouble:
require(value != null || strategy.descriptor.isNullable) { "Value was null for non-nullable type ${strategy.descriptor.serialName}" }
return FirebaseDecoder(value, decodeDouble).decodeSerializableValue(strategy)
}

expect fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decodeDouble: (value: Any?) -> Double?): CompositeDecoder
expect fun getPolymorphicType(value: Any?, discriminator: String): String

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

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

override fun decodeNull() = decodeNull(value)

@ExperimentalSerializationApi
override fun decodeInline(inlineDescriptor: SerialDescriptor) = FirebaseDecoder(value, decodeDouble)

override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
return decodeSerializableValuePolymorphic(value, decodeDouble, deserializer)
}
}

class FirebaseClassDecoder(
Expand All @@ -80,7 +83,7 @@ class FirebaseClassDecoder(
?: DECODE_DONE
}

open class FirebaseCompositeDecoder constructor(
open class FirebaseCompositeDecoder(
private val decodeDouble: (value: Any?) -> Double?,
private val size: Int,
private val get: (descriptor: SerialDescriptor, index: Int) -> Any?
Expand Down Expand Up @@ -134,6 +137,7 @@ open class FirebaseCompositeDecoder constructor(
@ExperimentalSerializationApi
override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder =
FirebaseDecoder(get(descriptor, index), decodeDouble)

}

private fun decodeString(value: Any?) = value.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,23 @@ inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean, pos
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply { encodeSerializableValue(it.firebaseSerializer(), it) }.value
}

expect fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder
expect fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder

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

var value: Any? = null

override val serializersModule = EmptySerializersModule
override fun beginStructure(descriptor: SerialDescriptor) = structureEncoder(descriptor)
private var polymorphicDiscriminator: String? = null

override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
val encoder = structureEncoder(descriptor)
if (polymorphicDiscriminator != null) {
encoder.encodePolymorphicClassDiscriminator(polymorphicDiscriminator!!, descriptor.serialName)
polymorphicDiscriminator = null
}
return encoder
}

override fun encodeBoolean(value: Boolean) {
this.value = value
Expand Down Expand Up @@ -73,9 +82,14 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive
this.value = value
}

@ExperimentalSerializationApi
override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder =
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)

override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
encodePolymorphically(serializer, value) {
polymorphicDiscriminator = it
}
}
}

abstract class TimestampEncoder(internal val positiveInfinity: Any) {
Expand All @@ -89,7 +103,8 @@ open class FirebaseCompositeEncoder constructor(
private val shouldEncodeElementDefault: Boolean,
positiveInfinity: Any,
private val end: () -> Unit = {},
private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit
private val setPolymorphicType: (String, String) -> Unit = { _, _ -> },
private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit,
): TimestampEncoder(positiveInfinity), CompositeEncoder {

override val serializersModule = EmptySerializersModule
Expand Down Expand Up @@ -153,6 +168,9 @@ open class FirebaseCompositeEncoder constructor(
@ExperimentalSerializationApi
override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder =
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
}

fun encodePolymorphicClassDiscriminator(discriminator: String, type: String) {
setPolymorphicType(discriminator, type)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package dev.gitlive.firebase

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

@Serializable
sealed class TestSealed {
@Serializable
@SerialName("child")
data class ChildClass(val map: Map<String, String>, val bool: Boolean = false): TestSealed()
}

class EncodersTest {
@Test
fun encodeMap() {
Expand All @@ -37,6 +45,12 @@ class EncodersTest {
nativeAssertEquals(nativeMapOf("map" to nativeMapOf("key" to "value"), "bool" to true, "nullableBool" to true), encoded)
}

@Test
fun encodeSealedClass() {
val encoded = encode<TestSealed>(TestSealed.serializer(), TestSealed.ChildClass(mapOf("key" to "value"), true), shouldEncodeElementDefault = true)
nativeAssertEquals(nativeMapOf("type" to "child", "map" to nativeMapOf("key" to "value"), "bool" to true), encoded)
}

@Test
fun decodeObject() {
val decoded = decode<TestData>(TestData.serializer(), nativeMapOf("map" to nativeMapOf("key" to "value")))
Expand All @@ -54,4 +68,10 @@ class EncodersTest {
val decoded = decode(TestData.serializer(), nativeMapOf("map" to mapOf("key" to "value"), "nullableBool" to null))
assertNull(decoded.nullableBool)
}

@Test
fun decodeSealedClass() {
val decoded = decode(TestSealed.serializer(), nativeMapOf("type" to "child", "map" to nativeMapOf("key" to "value"), "bool" to true))
assertEquals(TestSealed.ChildClass(mapOf("key" to "value"), true), decoded)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
package dev.gitlive.firebase

import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind

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

actual fun getPolymorphicType(value: Any?, discriminator: String): String =
(value as Map<*,*>)[discriminator] as String
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@

package dev.gitlive.firebase

import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.encoding.CompositeEncoder
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlin.collections.set

actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): CompositeEncoder = when(descriptor.kind as StructureKind) {
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
StructureKind.LIST -> mutableListOf<Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
StructureKind.MAP -> mutableListOf<Any?>()
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
StructureKind.CLASS, StructureKind.OBJECT -> mutableMapOf<Any?, Any?>()
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> mutableMapOf<Any?, Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it[descriptor.getElementName(index)] = value } }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity,
setPolymorphicType = { discriminator, type ->
it[discriminator] = type
},
set = { _, index, value -> it[descriptor.getElementName(index)] = value }
) }
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
package dev.gitlive.firebase

import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlin.js.Json

@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decodeDouble: (value: Any?) -> Double?): CompositeDecoder = when(descriptor.kind as StructureKind) {
StructureKind.CLASS, StructureKind.OBJECT -> (value as Json).let { json ->
actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decodeDouble: (value: Any?) -> Double?): CompositeDecoder = when(descriptor.kind) {
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> (value as Json).let { json ->
FirebaseClassDecoder(decodeDouble, js("Object").keys(value).length as Int, { json[it] != undefined }) {
desc, index -> json[desc.getElementName(index)]
}
Expand All @@ -24,4 +23,9 @@ actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decode
StructureKind.MAP -> (js("Object").entries(value) as Array<Array<Any>>).let {
FirebaseCompositeDecoder(decodeDouble, it.size) { _, index -> it[index/2].run { if(index % 2 == 0) get(0) else get(1) } }
}
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
}

@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
actual fun getPolymorphicType(value: Any?, discriminator: String): String =
(value as Json)[discriminator] as String
Loading

0 comments on commit 566d9aa

Please sign in to comment.