Skip to content

Commit

Permalink
Merge pull request #448 from splendo/feature/serialization-fixes
Browse files Browse the repository at this point in the history
Adding Serialization fixes
  • Loading branch information
nbransby authored Mar 29, 2024
2 parents d13f931 + f8eb4fd commit 71bcb81
Show file tree
Hide file tree
Showing 62 changed files with 2,649 additions and 1,394 deletions.
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,43 @@ data class City(val name: String)
Instances of these classes can now be passed [along with their serializer](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#introduction-to-serializers) to the SDK:

```kotlin
db.collection("cities").document("LA").set(City.serializer(), city, encodeDefaults = true)
db.collection("cities").document("LA").set(City.serializer(), city) { encodeDefaults = true }
```

The `encodeDefaults` parameter is optional and defaults to `true`, set this to false to omit writing optional properties if they are equal to theirs default values.
The `buildSettings` closure is optional and allows for configuring serialization behaviour.

Setting the `encodeDefaults` parameter is optional and defaults to `true`, set this to false to omit writing optional properties if they are equal to theirs default values.
Using [@EncodeDefault](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-encode-default/) on properties is a recommended way to locally override the behavior set with `encodeDefaults`.

You can also omit the serializer but this is discouraged due to a [current limitation on Kotlin/JS and Kotlin/Native](https://github.com/Kotlin/kotlinx.serialization/issues/1116#issuecomment-704342452)
You can also omit the serializer if it can be inferred using `serializer<KType>()`.
To support [contextual serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#contextual-serialization) or [open polymorphism](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism) the `serializersModule` can be overridden in the `buildSettings` closure:

```kotlin
@Serializable
abstract class AbstractCity {
abstract val name: String
}

@Serializable
@SerialName("capital")
data class Capital(override val name: String, val isSeatOfGovernment: Boolean) : AbstractCity()

val module = SerializersModule {
polymorphic(AbstractCity::class, AbstractCity.serializer()) {
subclass(Capital::class, Capital.serializer())
}
}

val city = Capital("London", true)
db.collection("cities").document("UK").set(AbstractCity.serializer(), city) {
encodeDefaults = true
serializersModule = module

}
val storedCity = db.collection("cities").document("UK").get().data(AbstractCity.serializer()) {
serializersModule = module
}
```

<h4><a href="https://firebase.google.com/docs/firestore/manage-data/add-data#server_timestamp">Server Timestamp</a></h3>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,41 @@

package dev.gitlive.firebase

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

actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor): CompositeDecoder = when(descriptor.kind) {
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> (value as Map<*, *>).let { map ->
FirebaseClassDecoder(map.size, { map.containsKey(it) }) { desc, index ->
val elementName = desc.getElementName(index)
if (desc.kind is PolymorphicKind && elementName == "value") {
map
} else {
map[desc.getElementName(index)]
}
}
}
StructureKind.LIST ->
when(value) {
is List<*> -> value
is Map<*, *> -> value.asSequence()
.sortedBy { (it) -> it.toString().toIntOrNull() }
.map { (_, it) -> it }
.toList()
else -> error("unexpected type, got $value when expecting a list")
}
.let { FirebaseCompositeDecoder(it.size) { _, index -> it[index] } }
StructureKind.MAP -> (value as Map<*, *>).entries.toList().let {
FirebaseCompositeDecoder(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 FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, polymorphicIsNested: Boolean): CompositeDecoder = when (descriptor.kind) {
StructureKind.CLASS, StructureKind.OBJECT -> decodeAsMap(false)
StructureKind.LIST -> (value as? List<*>).orEmpty().let {
FirebaseCompositeDecoder(it.size, settings) { _, index -> it[index] }
}

StructureKind.MAP -> (value as? Map<*, *>).orEmpty().entries.toList().let {
FirebaseCompositeDecoder(
it.size,
settings
) { _, index -> it[index / 2].run { if (index % 2 == 0) key else value } }
}

is PolymorphicKind -> decodeAsMap(polymorphicIsNested)
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
(value as? Map<*,*>).orEmpty()[discriminator] as String

private fun FirebaseDecoder.decodeAsMap(isNestedPolymorphic: Boolean): CompositeDecoder = (value as? Map<*, *>).orEmpty().let { map ->
FirebaseClassDecoder(map.size, settings, { map.containsKey(it) }) { desc, index ->
if (isNestedPolymorphic) {
if (desc.getElementName(index) == "value")
map
else {
map[desc.getElementName(index)]
}
} else {
map[desc.getElementName(index)]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,30 @@

package dev.gitlive.firebase

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

actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
StructureKind.LIST -> mutableListOf<Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it.add(index, value) } }
.let { FirebaseCompositeEncoder(settings) { _, index, value -> it.add(index, value) } }
StructureKind.MAP -> mutableListOf<Any?>()
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, { 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,
.let { FirebaseCompositeEncoder(settings, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
StructureKind.CLASS, StructureKind.OBJECT -> encodeAsMap(descriptor)
is PolymorphicKind -> encodeAsMap(descriptor)
else -> TODO("The firebase-kotlin-sdk does not support $descriptor for serialization yet")
}

private fun FirebaseEncoder.encodeAsMap(descriptor: SerialDescriptor): FirebaseCompositeEncoder = mutableMapOf<Any?, Any?>()
.also { value = it }
.let {
FirebaseCompositeEncoder(
settings,
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,70 @@
package dev.gitlive.firebase

import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule

/**
* Settings used to configure encoding/decoding
*/
sealed class EncodeDecodeSettings {

/**
* The [SerializersModule] to use for serialization. This allows for polymorphic serialization on runtime
*/
abstract val serializersModule: SerializersModule
}

/**
* [EncodeDecodeSettings] used when encoding an object
* @property encodeDefaults if `true` this will explicitly encode elements even if they are their default value
* @param serializersModule the [SerializersModule] to use for serialization. This allows for polymorphic serialization on runtime
*/
data class EncodeSettings internal constructor(
val encodeDefaults: Boolean,
override val serializersModule: SerializersModule,
) : EncodeDecodeSettings() {

interface Builder {
var encodeDefaults: Boolean
var serializersModule: SerializersModule

}

@PublishedApi
internal class BuilderImpl : Builder {
override var encodeDefaults: Boolean = true
override var serializersModule: SerializersModule = EmptySerializersModule()
}
}

/**
* [EncodeDecodeSettings] used when decoding an object
* @param serializersModule the [SerializersModule] to use for deserialization. This allows for polymorphic serialization on runtime
*/
data class DecodeSettings internal constructor(
override val serializersModule: SerializersModule = EmptySerializersModule(),
) : EncodeDecodeSettings() {

interface Builder {
var serializersModule: SerializersModule
}

@PublishedApi
internal class BuilderImpl : Builder {
override var serializersModule: SerializersModule = EmptySerializersModule()
}
}

interface EncodeDecodeSettingsBuilder : EncodeSettings.Builder, DecodeSettings.Builder

@PublishedApi
internal class EncodeDecodeSettingsBuilderImpl : EncodeDecodeSettingsBuilder {

override var encodeDefaults: Boolean = true
override var serializersModule: SerializersModule = EmptySerializersModule()
}

@PublishedApi
internal fun EncodeSettings.Builder.buildEncodeSettings(): EncodeSettings = EncodeSettings(encodeDefaults, serializersModule)
@PublishedApi
internal fun DecodeSettings.Builder.buildDecodeSettings(): DecodeSettings = DecodeSettings(serializersModule)
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ internal fun <T> FirebaseEncoder.encodePolymorphically(
value: T,
ifPolymorphic: (String) -> Unit
) {
// If serializer is not an AbstractPolymorphicSerializer we can just use the regular serializer
// This will result in calling structureEncoder for complicated structures
// For PolymorphicKind this will first encode the polymorphic discriminator as a String and the remaining StructureKind.Class as a map of key-value pairs
// This will result in a list structured like: (type, { classKey = classValue })
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)
Expand All @@ -32,15 +37,15 @@ internal fun <T> FirebaseDecoder.decodeSerializableValuePolymorphic(
value: Any?,
deserializer: DeserializationStrategy<T>,
): T {
// If deserializer is not an AbstractPolymorphicSerializer we can just use the regular serializer
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),
structureDecoder(deserializer.descriptor, false),
type
) as DeserializationStrategy<T>
return actualDeserializer.deserialize(this)
Expand All @@ -55,4 +60,3 @@ internal fun SerialDescriptor.classDiscriminator(): String {
}
return "type"
}

Loading

0 comments on commit 71bcb81

Please sign in to comment.