Skip to content

Commit

Permalink
Serialization improvements (#312)
Browse files Browse the repository at this point in the history
* fix encoding of the 1st value in updates

* create wrappers for FieldValue and Timestamp and update serialization

* create a wrapper for ServerValue and update serialization

* remove Double.POSITIVE_INFINITY to Timestamp workaround

* added a double to timestamp serializer to support a legacy behaviour

* update readme and cleanup

* fix timestamp to milliseconds conversion
  • Loading branch information
vpodlesnyak authored Apr 8, 2023
1 parent d7ea33c commit 37a0176
Show file tree
Hide file tree
Showing 32 changed files with 696 additions and 300 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,21 @@ You can also omit the serializer but this is discouraged due to a [current limit

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

[Firestore](https://firebase.google.com/docs/reference/kotlin/com/google/firebase/firestore/FieldValue?hl=en#serverTimestamp()) and the [Realtime Database](https://firebase.google.com/docs/reference/android/com/google/firebase/database/ServerValue#TIMESTAMP) provide a sentinel value you can use to set a field in your document to a server timestamp. So you can use these values in custom classes they are of type `Double`:
[Firestore](https://firebase.google.com/docs/reference/kotlin/com/google/firebase/firestore/FieldValue?hl=en#serverTimestamp()) and the [Realtime Database](https://firebase.google.com/docs/reference/android/com/google/firebase/database/ServerValue#TIMESTAMP) provide a sentinel value you can use to set a field in your document to a server timestamp. So you can use these values in custom classes:

```kotlin
@Serializable
data class Post(
// In case using Realtime Database.
val timestamp: Double = ServerValue.TIMESTAMP,
val timestamp = ServerValue.TIMESTAMP,
// In case using Cloud Firestore.
val timestamp: Double = FieldValue.serverTimestamp,
val timestamp: Timestamp = Timestamp.ServerTimestamp,
// or
val alternativeTimestamp = FieldValue.serverTimestamp,
// or
@Serializable(with = DoubleAsTimestampSerializer::class),
val doubleTimestamp: Double = DoubleAsTimestampSerializer.serverTimestamp
)

```

<h4>Polymorphic serialization (sealed classes)</h4>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ 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) {
actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor): 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)] }
FirebaseClassDecoder(map.size, { map.containsKey(it) }) { desc, index -> map[desc.getElementName(index)] }
}
StructureKind.LIST -> (value as List<*>).let {
FirebaseCompositeDecoder(decodeDouble, it.size) { _, index -> it[index] }
FirebaseCompositeDecoder(it.size) { _, index -> it[index] }
}
StructureKind.MAP -> (value as Map<*, *>).entries.toList().let {
FirebaseCompositeDecoder(decodeDouble, it.size) { _, index -> it[index/2].run { if(index % 2 == 0) key else value } }
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")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import kotlin.collections.set
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) } }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, 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) } }
.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, positiveInfinity,
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault,
setPolymorphicType = { discriminator, type ->
it[discriminator] = type
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ internal fun <T> FirebaseEncoder.encodePolymorphically(
@Suppress("UNCHECKED_CAST")
internal fun <T> FirebaseDecoder.decodeSerializableValuePolymorphic(
value: Any?,
decodeDouble: (value: Any?) -> Double?,
deserializer: DeserializationStrategy<T>,
): T {
if (deserializer !is AbstractPolymorphicSerializer<*>) {
Expand All @@ -41,7 +40,7 @@ internal fun <T> FirebaseDecoder.decodeSerializableValuePolymorphic(
val discriminator = deserializer.descriptor.classDiscriminator()
val type = getPolymorphicType(value, discriminator)
val actualDeserializer = casted.findPolymorphicSerializerOrNull(
structureDecoder(deserializer.descriptor, decodeDouble),
structureDecoder(deserializer.descriptor),
type
) as DeserializationStrategy<T>
return actualDeserializer.deserialize(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,28 @@ import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer

@Suppress("UNCHECKED_CAST")
inline fun <reified T> decode(value: Any?, noinline decodeDouble: (value: Any?) -> Double? = { null }): T {
inline fun <reified T> decode(value: Any?): T {
val strategy = serializer<T>()
return decode(strategy as DeserializationStrategy<T>, value, decodeDouble)
return decode(strategy as DeserializationStrategy<T>, value)
}

fun <T> decode(strategy: DeserializationStrategy<T>, value: Any?, decodeDouble: (value: Any?) -> Double? = { null }): T {
fun <T> decode(strategy: DeserializationStrategy<T>, value: Any?): T {
require(value != null || strategy.descriptor.isNullable) { "Value was null for non-nullable type ${strategy.descriptor.serialName}" }
return FirebaseDecoder(value, decodeDouble).decodeSerializableValue(strategy)
return FirebaseDecoder(value).decodeSerializableValue(strategy)
}
expect fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor, decodeDouble: (value: Any?) -> Double?): CompositeDecoder
expect fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor): CompositeDecoder
expect fun getPolymorphicType(value: Any?, discriminator: String): String

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

override val serializersModule: SerializersModule
get() = EmptySerializersModule

override fun beginStructure(descriptor: SerialDescriptor) = structureDecoder(descriptor, decodeDouble)
override fun beginStructure(descriptor: SerialDescriptor) = structureDecoder(descriptor)

override fun decodeString() = decodeString(value)

override fun decodeDouble() = decodeDouble(value, decodeDouble)
override fun decodeDouble() = decodeDouble(value)

override fun decodeLong() = decodeLong(value)

Expand All @@ -59,19 +59,18 @@ class FirebaseDecoder(internal val value: Any?, private val decodeDouble: (value

override fun decodeNull() = decodeNull(value)

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

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

class FirebaseClassDecoder(
decodeDouble: (value: Any?) -> Double?,
size: Int,
private val containsKey: (name: String) -> Boolean,
get: (descriptor: SerialDescriptor, index: Int) -> Any?
) : FirebaseCompositeDecoder(decodeDouble, size, get) {
) : FirebaseCompositeDecoder(size, get) {
private var index: Int = 0

override fun decodeSequentially() = false
Expand All @@ -84,7 +83,6 @@ class FirebaseClassDecoder(
}

open class FirebaseCompositeDecoder(
private val decodeDouble: (value: Any?) -> Double?,
private val size: Int,
private val get: (descriptor: SerialDescriptor, index: Int) -> Any?
): CompositeDecoder {
Expand All @@ -102,15 +100,15 @@ open class FirebaseCompositeDecoder(
index: Int,
deserializer: DeserializationStrategy<T>,
previousValue: T?
) = deserializer.deserialize(FirebaseDecoder(get(descriptor, index), decodeDouble))
) = deserializer.deserialize(FirebaseDecoder(get(descriptor, index)))

override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) = decodeBoolean(get(descriptor, index))

override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) = decodeByte(get(descriptor, index))

override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) = decodeChar(get(descriptor, index))

override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = decodeDouble(get(descriptor, index), decodeDouble)
override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = decodeDouble(get(descriptor, index))

override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) = decodeFloat(get(descriptor, index))

Expand All @@ -136,16 +134,16 @@ open class FirebaseCompositeDecoder(

@ExperimentalSerializationApi
override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder =
FirebaseDecoder(get(descriptor, index), decodeDouble)
FirebaseDecoder(get(descriptor, index))

}

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

private fun decodeDouble(value: Any?, decodeDouble: (value: Any?) -> Double?) = when(value) {
private fun decodeDouble(value: Any?) = when(value) {
is Number -> value.toDouble()
is String -> value.toDouble()
else -> decodeDouble(value) ?: throw SerializationException("Expected $value to be double")
else -> throw SerializationException("Expected $value to be double")
}

private fun decodeLong(value: Any?) = when(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import kotlinx.serialization.encoding.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.modules.EmptySerializersModule

fun <T> encode(strategy: SerializationStrategy<T>, value: T, shouldEncodeElementDefault: Boolean, positiveInfinity: Any = Double.POSITIVE_INFINITY): Any? =
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply { encodeSerializableValue(strategy, value) }.value//.also { println("encoded $it") }
fun <T> encode(strategy: SerializationStrategy<T>, value: T, shouldEncodeElementDefault: Boolean): Any? =
FirebaseEncoder(shouldEncodeElementDefault).apply { encodeSerializableValue(strategy, value) }.value//.also { println("encoded $it") }

inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean, positiveInfinity: Any = Double.POSITIVE_INFINITY): Any? = value?.let {
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply { encodeSerializableValue(it.firebaseSerializer(), it) }.value
inline fun <reified T> encode(value: T, shouldEncodeElementDefault: Boolean): Any? = value?.let {
FirebaseEncoder(shouldEncodeElementDefault).apply { encodeSerializableValue(it.firebaseSerializer(), it) }.value
}

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

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

var value: Any? = null

Expand Down Expand Up @@ -47,7 +47,7 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive
}

override fun encodeDouble(value: Double) {
this.value = encodeTimestamp(value)
this.value = value
}

override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
Expand Down Expand Up @@ -83,7 +83,7 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive
}

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

override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
encodePolymorphically(serializer, value) {
Expand All @@ -92,20 +92,12 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive
}
}

abstract class TimestampEncoder(internal val positiveInfinity: Any) {
fun encodeTimestamp(value: Double) = when(value) {
Double.POSITIVE_INFINITY -> positiveInfinity
else -> value
}
}

open class FirebaseCompositeEncoder constructor(
private val shouldEncodeElementDefault: Boolean,
positiveInfinity: Any,
private val end: () -> Unit = {},
private val setPolymorphicType: (String, String) -> Unit = { _, _ -> },
private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit,
): TimestampEncoder(positiveInfinity), CompositeEncoder {
): CompositeEncoder {

override val serializersModule = EmptySerializersModule

Expand All @@ -128,7 +120,7 @@ open class FirebaseCompositeEncoder constructor(
descriptor,
index,
value?.let {
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply {
FirebaseEncoder(shouldEncodeElementDefault).apply {
encodeSerializableValue(serializer, value)
}.value
}
Expand All @@ -142,7 +134,7 @@ open class FirebaseCompositeEncoder constructor(
) = set(
descriptor,
index,
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply {
FirebaseEncoder(shouldEncodeElementDefault).apply {
encodeSerializableValue(serializer, value)
}.value
)
Expand All @@ -153,7 +145,7 @@ open class FirebaseCompositeEncoder constructor(

override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) = set(descriptor, index, value)

override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = set(descriptor, index, encodeTimestamp(value))
override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = set(descriptor, index, value)

override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) = set(descriptor, index, value)

Expand All @@ -167,7 +159,7 @@ open class FirebaseCompositeEncoder constructor(

@ExperimentalSerializationApi
override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder =
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
FirebaseEncoder(shouldEncodeElementDefault)

fun encodePolymorphicClassDiscriminator(discriminator: String, type: String) {
setPolymorphicType(discriminator, type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,30 @@ class FirebaseListSerializer : KSerializer<Iterable<Any?>> {
}
}

/**
* A special case of serializer for values natively supported by Firebase and
* don't require an additional encoding/decoding.
*/
abstract class SpecialValueSerializer<T>(
serialName: String,
private val toNativeValue: (T) -> Any?,
private val fromNativeValue: (Any?) -> T
) : KSerializer<T> {
override val descriptor = buildClassSerialDescriptor(serialName) { }

override fun serialize(encoder: Encoder, value: T) {
if (encoder is FirebaseEncoder) {
encoder.value = toNativeValue(value)
} else {
throw SerializationException("This serializer must be used with FirebaseEncoder")
}
}

override fun deserialize(decoder: Decoder): T {
return if (decoder is FirebaseDecoder) {
fromNativeValue(decoder.value)
} else {
throw SerializationException("This serializer must be used with FirebaseDecoder")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ 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) {
actual fun FirebaseDecoder.structureDecoder(descriptor: SerialDescriptor): 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)] }
FirebaseClassDecoder(map.size, { map.containsKey(it) }) { desc, index -> map[desc.getElementName(index)] }
}
StructureKind.LIST -> (value as List<*>).let {
FirebaseCompositeDecoder(decodeDouble, it.size) { _, index -> it[index] }
FirebaseCompositeDecoder(it.size) { _, index -> it[index] }
}
StructureKind.MAP -> (value as Map<*, *>).entries.toList().let {
FirebaseCompositeDecoder(decodeDouble, it.size) { _, index -> it[index/2].run { if(index % 2 == 0) key else value } }
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")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import kotlin.collections.set
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) } }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, 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) } }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> mutableMapOf<Any?, Any?>()
.also { value = it }
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity,
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault,
setPolymorphicType = { discriminator, type ->
it[discriminator] = type
},
Expand Down
Loading

0 comments on commit 37a0176

Please sign in to comment.