Firestore Object Mapping made easy
At work we needed to read data from the Firestore into our Mobile app.
In Swift you can simply slap the Decodable protocol onto the struct and read it using .data(as:with:) and the data from Firestore is automatically mapped to the correct type and throws if decoding fails.
Problem
The Firebase Android SDK does come with a toObject() function but it doesn’t properly support parsing complex structures (think kotlinx.datetime.Instant) and aliasing enum values. In addition to that it is overly slow because it is reflection based.
Tho other option is to read it via the .data() function but you merely get the values as a Map<String, Any?>` and would then have to validate the data (or throw) and then manually invoke the constructor of the data class, which amounts to a lot of unnecessary boilerplate:
enum class Category {
A, B;
companion object {
fun decode(value: String): Category? {
return when(value) {
"CATEGORY_A" -> A
"CATEGORY_B" -> B
else -> null
}
}
}
}
data class Test(
val category: Category,
val name: String
)
fun decodeTest(snapshot: DocumentSnapshot): Test {
val data = snapshot.data()
?: throw Error("decodeTest: missing document data")
val categoryRaw = data["category"] as? String
?: throw Error("decodeTest: missing category")
val category = Category.decode(categoryRaw)
?: throw Error("decodeTest: invalid category")
val name = data["name"] as? String
?: throw Error("decodeTest: missing name")
return Test(
category = category,
name = name
)
}
Because we had to implement this over and over we started to extract helper funcs to aid with the decoding and eventually ended up with a small library that uses a Kotlin Serialization Decoder and thus can make use of all the nice affordances that brings: @SerialName, custom Serializers (@Serializable(with)), polymorphic Serializers, …
Federmappe for Firestore
With the new lib all that is needed is to define serializable data classes:
import de.sipgate.federmappe.firestore.toObject
@Serializable
enum class Category {
@SerialName("CATEGORY_A") A,
@SerialName("CATEGORY_B") B;
}
@Serializable
data class Test(
val category: Category,
val name: String
)
fun decodeTest(snapshot: DocumentSnapshot): Test {
return snapshot.toObject<Test>()
}
All the boilerplate above is now handled by Kotlin Serialization’s generated serializers. Instead of relying on reflection to inspect and map properties at runtime, the generated code reads the values, performs the necessary casts, and invokes the constructors directly. This avoids the overhead of reflective property mapping and makes decoding more efficient.
Federmappe for Realtime Database
Firestore documents can contain nested maps and arrays, while subcollections are separate collections associated with a document. Realtime DB stores data as a JSON-like tree, where numeric child keys are often used for list-like data.
To reuse the same Kotlin Serialization decoder, the library iterates over the Realtime Database data and constructs the corresponding intermediate structures expected by the decoder.
About the name
The name was originally suggested by a colleague and means pencil case in German. It reads vaguely similar to F[irebase] Mappe[r] and wasn’t yet used by anything else.
What’s left to do
I am not sure if we will ever implement the encode/write path, but the read path is pretty stable and used in production by thousands of our users every day.
We still need to publish this library to Maven Central. Currently it is hosted on GitHub Packages and unfortunately needs a GitHub Token to access.