Skip to content

Commit 37a0176

Browse files
authored
Serialization improvements (#312)
* 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
1 parent d7ea33c commit 37a0176

File tree

32 files changed

+696
-300
lines changed

32 files changed

+696
-300
lines changed

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,21 @@ You can also omit the serializer but this is discouraged due to a [current limit
9393

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

96-
[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`:
96+
[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:
9797

9898
```kotlin
9999
@Serializable
100100
data class Post(
101101
// In case using Realtime Database.
102-
val timestamp: Double = ServerValue.TIMESTAMP,
102+
val timestamp = ServerValue.TIMESTAMP,
103103
// In case using Cloud Firestore.
104-
val timestamp: Double = FieldValue.serverTimestamp,
104+
val timestamp: Timestamp = Timestamp.ServerTimestamp,
105+
// or
106+
val alternativeTimestamp = FieldValue.serverTimestamp,
107+
// or
108+
@Serializable(with = DoubleAsTimestampSerializer::class),
109+
val doubleTimestamp: Double = DoubleAsTimestampSerializer.serverTimestamp
105110
)
106-
107111
```
108112

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

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import kotlinx.serialization.descriptors.PolymorphicKind
99
import kotlinx.serialization.descriptors.SerialDescriptor
1010
import kotlinx.serialization.descriptors.StructureKind
1111

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

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import kotlin.collections.set
1111
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
1212
StructureKind.LIST -> mutableListOf<Any?>()
1313
.also { value = it }
14-
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
14+
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it.add(index, value) } }
1515
StructureKind.MAP -> mutableListOf<Any?>()
16-
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
16+
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
1717
StructureKind.CLASS, StructureKind.OBJECT -> mutableMapOf<Any?, Any?>()
1818
.also { value = it }
19-
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity,
19+
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault,
2020
setPolymorphicType = { discriminator, type ->
2121
it[discriminator] = type
2222
},

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ internal fun <T> FirebaseEncoder.encodePolymorphically(
3030
@Suppress("UNCHECKED_CAST")
3131
internal fun <T> FirebaseDecoder.decodeSerializableValuePolymorphic(
3232
value: Any?,
33-
decodeDouble: (value: Any?) -> Double?,
3433
deserializer: DeserializationStrategy<T>,
3534
): T {
3635
if (deserializer !is AbstractPolymorphicSerializer<*>) {
@@ -41,7 +40,7 @@ internal fun <T> FirebaseDecoder.decodeSerializableValuePolymorphic(
4140
val discriminator = deserializer.descriptor.classDiscriminator()
4241
val type = getPolymorphicType(value, discriminator)
4342
val actualDeserializer = casted.findPolymorphicSerializerOrNull(
44-
structureDecoder(deserializer.descriptor, decodeDouble),
43+
structureDecoder(deserializer.descriptor),
4544
type
4645
) as DeserializationStrategy<T>
4746
return actualDeserializer.deserialize(this)

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

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,28 @@ import kotlinx.serialization.modules.SerializersModule
1616
import kotlinx.serialization.serializer
1717

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

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

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

3333
override val serializersModule: SerializersModule
3434
get() = EmptySerializersModule
3535

36-
override fun beginStructure(descriptor: SerialDescriptor) = structureDecoder(descriptor, decodeDouble)
36+
override fun beginStructure(descriptor: SerialDescriptor) = structureDecoder(descriptor)
3737

3838
override fun decodeString() = decodeString(value)
3939

40-
override fun decodeDouble() = decodeDouble(value, decodeDouble)
40+
override fun decodeDouble() = decodeDouble(value)
4141

4242
override fun decodeLong() = decodeLong(value)
4343

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

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

62-
override fun decodeInline(inlineDescriptor: SerialDescriptor) = FirebaseDecoder(value, decodeDouble)
62+
override fun decodeInline(inlineDescriptor: SerialDescriptor) = FirebaseDecoder(value)
6363

6464
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
65-
return decodeSerializableValuePolymorphic(value, decodeDouble, deserializer)
65+
return decodeSerializableValuePolymorphic(value, deserializer)
6666
}
6767
}
6868

6969
class FirebaseClassDecoder(
70-
decodeDouble: (value: Any?) -> Double?,
7170
size: Int,
7271
private val containsKey: (name: String) -> Boolean,
7372
get: (descriptor: SerialDescriptor, index: Int) -> Any?
74-
) : FirebaseCompositeDecoder(decodeDouble, size, get) {
73+
) : FirebaseCompositeDecoder(size, get) {
7574
private var index: Int = 0
7675

7776
override fun decodeSequentially() = false
@@ -84,7 +83,6 @@ class FirebaseClassDecoder(
8483
}
8584

8685
open class FirebaseCompositeDecoder(
87-
private val decodeDouble: (value: Any?) -> Double?,
8886
private val size: Int,
8987
private val get: (descriptor: SerialDescriptor, index: Int) -> Any?
9088
): CompositeDecoder {
@@ -102,15 +100,15 @@ open class FirebaseCompositeDecoder(
102100
index: Int,
103101
deserializer: DeserializationStrategy<T>,
104102
previousValue: T?
105-
) = deserializer.deserialize(FirebaseDecoder(get(descriptor, index), decodeDouble))
103+
) = deserializer.deserialize(FirebaseDecoder(get(descriptor, index)))
106104

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

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

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

113-
override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = decodeDouble(get(descriptor, index), decodeDouble)
111+
override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = decodeDouble(get(descriptor, index))
114112

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

@@ -136,16 +134,16 @@ open class FirebaseCompositeDecoder(
136134

137135
@ExperimentalSerializationApi
138136
override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder =
139-
FirebaseDecoder(get(descriptor, index), decodeDouble)
137+
FirebaseDecoder(get(descriptor, index))
140138

141139
}
142140

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

145-
private fun decodeDouble(value: Any?, decodeDouble: (value: Any?) -> Double?) = when(value) {
143+
private fun decodeDouble(value: Any?) = when(value) {
146144
is Number -> value.toDouble()
147145
is String -> value.toDouble()
148-
else -> decodeDouble(value) ?: throw SerializationException("Expected $value to be double")
146+
else -> throw SerializationException("Expected $value to be double")
149147
}
150148

151149
private fun decodeLong(value: Any?) = when(value) {

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

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ import kotlinx.serialization.encoding.*
99
import kotlinx.serialization.descriptors.*
1010
import kotlinx.serialization.modules.EmptySerializersModule
1111

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

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

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

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

2323
var value: Any? = null
2424

@@ -47,7 +47,7 @@ class FirebaseEncoder(internal val shouldEncodeElementDefault: Boolean, positive
4747
}
4848

4949
override fun encodeDouble(value: Double) {
50-
this.value = encodeTimestamp(value)
50+
this.value = value
5151
}
5252

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

8585
override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder =
86-
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
86+
FirebaseEncoder(shouldEncodeElementDefault)
8787

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

95-
abstract class TimestampEncoder(internal val positiveInfinity: Any) {
96-
fun encodeTimestamp(value: Double) = when(value) {
97-
Double.POSITIVE_INFINITY -> positiveInfinity
98-
else -> value
99-
}
100-
}
101-
10295
open class FirebaseCompositeEncoder constructor(
10396
private val shouldEncodeElementDefault: Boolean,
104-
positiveInfinity: Any,
10597
private val end: () -> Unit = {},
10698
private val setPolymorphicType: (String, String) -> Unit = { _, _ -> },
10799
private val set: (descriptor: SerialDescriptor, index: Int, value: Any?) -> Unit,
108-
): TimestampEncoder(positiveInfinity), CompositeEncoder {
100+
): CompositeEncoder {
109101

110102
override val serializersModule = EmptySerializersModule
111103

@@ -128,7 +120,7 @@ open class FirebaseCompositeEncoder constructor(
128120
descriptor,
129121
index,
130122
value?.let {
131-
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply {
123+
FirebaseEncoder(shouldEncodeElementDefault).apply {
132124
encodeSerializableValue(serializer, value)
133125
}.value
134126
}
@@ -142,7 +134,7 @@ open class FirebaseCompositeEncoder constructor(
142134
) = set(
143135
descriptor,
144136
index,
145-
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity).apply {
137+
FirebaseEncoder(shouldEncodeElementDefault).apply {
146138
encodeSerializableValue(serializer, value)
147139
}.value
148140
)
@@ -153,7 +145,7 @@ open class FirebaseCompositeEncoder constructor(
153145

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

156-
override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = set(descriptor, index, encodeTimestamp(value))
148+
override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = set(descriptor, index, value)
157149

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

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

168160
@ExperimentalSerializationApi
169161
override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder =
170-
FirebaseEncoder(shouldEncodeElementDefault, positiveInfinity)
162+
FirebaseEncoder(shouldEncodeElementDefault)
171163

172164
fun encodePolymorphicClassDiscriminator(discriminator: String, type: String) {
173165
setPolymorphicType(discriminator, type)

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,30 @@ class FirebaseListSerializer : KSerializer<Iterable<Any?>> {
104104
}
105105
}
106106

107+
/**
108+
* A special case of serializer for values natively supported by Firebase and
109+
* don't require an additional encoding/decoding.
110+
*/
111+
abstract class SpecialValueSerializer<T>(
112+
serialName: String,
113+
private val toNativeValue: (T) -> Any?,
114+
private val fromNativeValue: (Any?) -> T
115+
) : KSerializer<T> {
116+
override val descriptor = buildClassSerialDescriptor(serialName) { }
117+
118+
override fun serialize(encoder: Encoder, value: T) {
119+
if (encoder is FirebaseEncoder) {
120+
encoder.value = toNativeValue(value)
121+
} else {
122+
throw SerializationException("This serializer must be used with FirebaseEncoder")
123+
}
124+
}
125+
126+
override fun deserialize(decoder: Decoder): T {
127+
return if (decoder is FirebaseDecoder) {
128+
fromNativeValue(decoder.value)
129+
} else {
130+
throw SerializationException("This serializer must be used with FirebaseDecoder")
131+
}
132+
}
133+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import kotlinx.serialization.descriptors.PolymorphicKind
99
import kotlinx.serialization.descriptors.SerialDescriptor
1010
import kotlinx.serialization.descriptors.StructureKind
1111

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

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import kotlin.collections.set
1313
actual fun FirebaseEncoder.structureEncoder(descriptor: SerialDescriptor): FirebaseCompositeEncoder = when(descriptor.kind) {
1414
StructureKind.LIST -> mutableListOf<Any?>()
1515
.also { value = it }
16-
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity) { _, index, value -> it.add(index, value) } }
16+
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault) { _, index, value -> it.add(index, value) } }
1717
StructureKind.MAP -> mutableListOf<Any?>()
18-
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
18+
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, { value = it.chunked(2).associate { (k, v) -> k to v } }) { _, _, value -> it.add(value) } }
1919
StructureKind.CLASS, StructureKind.OBJECT, PolymorphicKind.SEALED -> mutableMapOf<Any?, Any?>()
2020
.also { value = it }
21-
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault, positiveInfinity,
21+
.let { FirebaseCompositeEncoder(shouldEncodeElementDefault,
2222
setPolymorphicType = { discriminator, type ->
2323
it[discriminator] = type
2424
},

0 commit comments

Comments
 (0)