Skip to content

Adding Serialization fixes #448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
816bd62
Adding Serialization fixes
Daeda88 Dec 19, 2023
e126519
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 2, 2024
69e4516
Ensure API stability
Daeda88 Jan 3, 2024
582532d
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 3, 2024
fae5299
Track for JS
Daeda88 Jan 3, 2024
0d252b5
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 5, 2024
a4a7150
Use named arguments for settings
Daeda88 Jan 12, 2024
0c6e22c
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 12, 2024
8305507
Use builders instead
Daeda88 Jan 12, 2024
045e127
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 12, 2024
3c9f7ff
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Jan 19, 2024
4ad4d13
Remove Async classes + add inline for builder funs
Daeda88 Jan 22, 2024
46d7c17
Fixed Database runTransaction
Daeda88 Jan 22, 2024
bf709a3
Fixed Android crash, updated tests + cleanup
Daeda88 Jan 23, 2024
3021502
Update Readme
Daeda88 Jan 23, 2024
59b4522
Some renames + readme update
Daeda88 Jan 27, 2024
7a2ae5d
Add value class fix
Daeda88 Jan 27, 2024
2fb7f58
Made helper methods internal
Daeda88 Jan 27, 2024
0ca5675
Slight optimization of Value class tests
Daeda88 Jan 27, 2024
1995071
Small test improvement
Daeda88 Jan 29, 2024
87d4ae2
Decode polymorphic using elementName rather than index
Daeda88 Jan 29, 2024
71bcce4
Also test nested data class encoding
Daeda88 Jan 29, 2024
26d7ea9
Merge remote-tracking branch 'GitLiveApp/master' into feature/seriali…
Daeda88 Mar 1, 2024
f75b240
Use Wrappers rather than Abstract classes
Daeda88 Mar 1, 2024
f8eb4fd
Use wrappes + extension methods instead of abstract class
Daeda88 Mar 1, 2024
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
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