Skip to content

Serialization improvements #312

New issue

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

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

Already on GitHub? # to your account

Merged
merged 8 commits into from
Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

special value handling is replaced by using a custom serializer

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>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this PR it's used for Realtime Database ServerValue and Firestore FieldValue and Timestamp. Firestore also supports storing GeoPoints and DocumentReferences natively, but it's out of scope of this PR

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