Kotlin Convert Data Class to Map

Dec 16, 2021

This is not a straightforward solution, as my use case is slightly differ from the common one.

You will need to add kotlin-reflect.

depedencies {
    implementation "org.jetbrains.kotlin:kotlin-reflect"
}

A lot of the annotation is for Firestore: @IgnoreExtraProperties, @PropertyName, which is not necessary for your use case.

@IgnoreExtraPropertiesdata class Card(    override var id: String? = null,    var modified: Timestamp? = null,    var title: String? = null,    @get:PropertyName(IMAGE_IDS) @set:PropertyName(IMAGE_IDS)    var imageIds: List<String>? = null,    // var place: Place? = null,) : FirestoreData {    companion object {        const val COLLECTION_NAME = "card"        const val MODIFIED = "modified"        const val TITLE = "title"        // const val PLACE = "place"        const val IMAGE_IDS = "image_ids"        fun colRef(firestore: FirebaseFirestore, uid: String) = UserPrivate.colRef(firestore).document(uid).collection(            COLLECTION_NAME        )        fun toObject(doc: DocumentSnapshot): Card? {            val item = doc.toObject<Card>()            item?.id = doc.id            return item        }    }    val isNew: Boolean        get() = id == null    override fun getData(filterNull: Boolean): Map<String, Any?> {        return FirestoreDataUtil.toMap(this, excludeNames = listOf(::COLLECTION_NAME.name), filterNull)    }}

An Interfect class to indicate support to convert data class to map.

interface FirestoreData {    var id: String? // optional    fun getData(filterNull: Boolean = true) : Map<String, Any?>}

Process

  • Find all the constant public string in companion object to find all the field names (ability to exclude certain fields). With this method, I can add new property to this class where I don't want it to be included in the map.
  • Get the fields and their value based on these field names.
  • If object is instance of FirestoreData (like the commented Place field), call getData to get its underlying field.
class FirestoreDataUtil {    companion object {        fun toMap(instance: Any, excludeNames: List<String>, filterNull: Boolean = true) : Map<String, Any?> {            val findMember = instance::class.declaredMemberProperties                .filter{ it.visibility == KVisibility.PUBLIC }                .filterIsInstance<KProperty1<Any, *>>()                .associateBy(                    {                        val annotation = it.getter.findAnnotation<PropertyName>()                        annotation?.value ?: it.name                    },                    { it }                )            var data = instance::class.companionObject!!.declaredMemberProperties                .filter { it.visibility == KVisibility.PUBLIC && it.isConst && it.returnType == String::class.createType() && !excludeNames.contains(it.name) }                .mapNotNull {                    it.getter.call(this).toString()                }                .associateBy(                    { it },                    {                        // raise exception or return null to ignore                        val prop = findMember[it] ?: throw Exception("$it not found")                        val value = prop.get(instance)                        if (value != null) {                            /*                            if (value::class.isData) {                                value::class.memberFunctions.find {  func -> func.name == "toMap" }?.call(value, filterNull) ?: value                            }                             */                            // TODO("Check value is List<FirestoreData> or Map<Any, FirestoreData>")                            if (value is FirestoreData) {                                value.getData(filterNull)                            }                            else value                        } else null                    }                )            if (filterNull) {                data = data.filterValues { it != null }            }            return data        }    }}

Discussions

I didn't use Json or Kotlin Serialization approach because it require each field to be converted into basic type (e.g. Timestamp to epoch milli seconds). I need the value to maintain it's original type.

Originally I use Kotlin Reflect to find the member fields of the data class, but I might accidentally add a new property (e.g. isNew property) which I don't intend for it to end up in the map.

❤️ Is this article helpful?

Buy me a coffee ☕ or support my work via PayPal to keep this space 🖖 and ad-free.

Do send some 💖 to @d_luaz or share this article.

✨ By Desmond Lua

A dream boy who enjoys making apps, travelling and making youtube videos. Follow me on @d_luaz

👶 Apps I built

Travelopy - discover travel places in Malaysia, Singapore, Taiwan, Japan.