Firestore Custom Objects: Tips to handle Data Type Change

March 19, 2019

You can perform Firestore read/write using custom objects instead of map.

class Quote(
    @get:Exclude override var id : String? = null,
    @JvmField @PropertyName(CREATED) @ServerTimestamp val created: Timestamp? = null,
    @JvmField @PropertyName(CONTENT) var content: String? = "",
    @JvmField @PropertyName(LIKE_COUNT) val likeCount: Long? = 0,
    @JvmField @PropertyName(IS_ACTIVE) val isAcive: Boolean? = true

    companion object {
        const val COLLECTION_NAME = "quote"

        const val CREATED = "created"
        const val CONTENT = "content"
        const val LIKE_COUNT = "like_count"
        const val IS_ACTIVE = "is_active"
    }
}    

Get Firestore document as custom object.

FirebaseFirestore.getInstance().collection(Quote.COLLECTION_NAME)
    .document("document_id")
    .get()
    .addOnSuccessListener { documentSnapshot ->
        val item = documentSnapshot.toObject(Quote::class.java)
    }

Tip 1: make all property nullable

I think it is a good pratice to make all property nullable (even for Int, Long and Boolean). If the property for object class is not nullable and the actual value is null, calling toObject will cause java.lang.RuntimeException: Could not deserialize object..

NOTE: Null value might be set accidentally by other clients.

NOTE: If the property is not defined in Firestore, there is no issue.

Tip 2: don’t change underlying data type

We are using Boolean for is_active property. If you accidentally changed is_active to Long in Firestore, toObject will cause

java.lang.RuntimeException: Could not deserialize object. Failed to convert a value of type java.lang.Boolean to int (found in field ‘is_active’)

When I bump into this issue, I try to edit Firestore using the admin console to fix is_active back to Boolean. Sadly, the exception still persisted on the Android client, probably because query take priority of local cache (from previous fetch), and the exception is preventing latest data from being fetch.

NOTE: I am using com.google.firebase:firebase-firestore:18.1.0.

TIPS: If you want to change the underlying data type, change the property name as well (e.g. is_active_v2).

Tip 3: catch toObject exceptions

I think it is advisable to catch all toObject for potential exception, as underlying data type could be changed accidentally (e.g. Android using Boolean and Web Client accidentally assign Int). If you don’t catch the exception, a data type changed will cause the App in raise an unrecoverable exception.

Tip 4: runtime data conversion

You can try to write your own runtime custom toObject function.

The following toObject function handle changed of is_active data type from Boolean to Int.

class Quote(

    companion object {
        const val COLLECTION_NAME = "quote"

        const val CREATED = "created"
        const val CONTENT = "content"
        const val LIKE_COUNT = "like_count"
        const val IS_ACTIVE = "is_active"

        fun toObject(snapshot: DocumentSnapshot): Quote? {
            val isActive = snapshot[IS_ACTIVE]
            if (isActive is Boolean) { // if isActive is Boolean, convert to Int
                val data = snapshot.data!!
                data[IS_ACTIVE] = if (isActive) 1 else 0
                // runtime patch
                // sadly snapshot data cannot be modified directly, so we call an update
                snapshot.reference.update(mapOf(
                    IS_ACTIVE to data[IS_ACTIVE]
                ))
        
                // https://github.com/firebase/firebase-android-sdk/blob/master/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentSnapshot.java
                return try {
                    CustomClassMapper.convertToCustomClass(data, Quote::class.java)
                }
                catch (e: RuntimeException) {
                    Timber.e(e)
                    null
                }
            }
        }
    }
}

NOTE: I don’t think this is a good solution, but more of a emergency fix if the app is stuck in unrecoverable exception. While using your own custom toObject, you can’t use the convinient function of QuerySnapshot.
toObjects
.

NOTE: Sadly Firestore doesn’t support custom adapter for property data type conversion or parsing.

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.