Monday, 1 November 2021

Deserializing nested firebase firestore classes on Android

Issue Description

I'm trying to deserialize a simple firebase firestore document for an android app.
The issue is with kotlin nested classes deserialization. Or I should say, I have a class which would like to store as a kotlin data class the contents of a map on the remote document.
Even though the @PropertyName-supplied field names match with the associated class property names, and the non-nested class (called Bucket) receives all of the "primitive" data correctly, its nested classes just seem not to be filled in with the remote data, falling back to their empty-constructor values you must provide, thus creating a garbage object.

Implementation Details

The Firebase Bucket document data structure is as follows:

firebase data structure

I created the following class to represent the whole document:

@Parcelize
data class Bucket(
    @PropertyName("id")
    val id: String,

    @PropertyName("name")
    val name: String,
    @PropertyName("description")
    val description: String,

    @PropertyName("bucket_style")
    val style: BucketStyle,

    @PropertyName("bucket_dates")
    val dates: BucketDates
) : Parcelable {
    constructor() : this("mock.id", "mock.name", "mock.desc", BucketStyle(), BucketDates(), listOf())
}

As an example, with the BucketStyle class:

@Parcelize
data class BucketStyle(
    @PropertyName("icon_family")
    val iconFamily: Int,

    @PropertyName("icon_id")
    val iconId: Int,

    @PropertyName("color_id")
    val colorId: Int
) : Parcelable {
    constructor() : this(0, 0, 1)
}

And symmetrically:

@Parcelize
data class BucketDates(
    @PropertyName("creation_date_timestamp")
    val creationDate: Timestamp,

    @PropertyName("last_edit_timestamp")
    val lastEdit: Timestamp
) : Parcelable {
    constructor() : this(Timestamp.now(), Timestamp.now())
}

And I deserialize it (in reality all the documents in a collection using a real-time listener) using the default firebase-provided method (28.4.2 version of firebase-bom)

buckets.addSnapshotListener { data, error -> 
    val update: List<Bucket> = data.toObjects(Bucket::class.java)
}

And the inner classes are not populated, as shown in this debugger, as the server value differs, and they use their default one, while IDs, names and everything else "primitive" works correctly.
enter image description here

I do not get errors, just a logcat warning while executing:

W/Firestore: (23.0.4) [CustomClassMapper]: No setter/field for bucket_style found on class com.[redacted].model.bucket.Bucket
W/Firestore: (23.0.4) [CustomClassMapper]: No setter/field for bucket_dates found on class com.[redacted].model.bucket.Bucket

Additional Analysis

Those logs perplexed me, because I really have created getters and setters since Bucket is a data class. Or have I?

I decompiled the kotlin file to java and I realized that there is no getter named getbucket_style() for our object:

@PropertyName("bucket_style")
@NotNull
private final BucketStyle style;

@NotNull
public final BucketStyle getStyle() {
    return this.style;
}

We only have getStyle().

We can try to do a trick by renaming the kotlin style property to bucket_style (To match firestore document field name) this way in Bucket:

@PropertyName("bucket_style")
val bucket_style: BucketStyle,

The java decompilation becomes:

@PropertyName("bucket_style")
@NotNull
private final BucketStyle bucket_style;

@NotNull
public final BucketStyle getBucket_style() {
    return this.bucket_style;
}

But the warning stays constant. The generated getter has an uppercase "B" (that's the syntax convention for generating getters in kotlin data classes).
However, if I create a custom getter named getbucket_style() in the original Bucket, as in:

@Parcelize
data class Bucket(
    @PropertyName("id")
    val id: String,

    @PropertyName("name")
    val name: String,
    @PropertyName("description")
    val description: String,

    @PropertyName("bucket_style")
    val style: BucketStyle,

    @PropertyName("bucket_dates")
    val dates: BucketDates
) : Parcelable {

    fun getbucket_style() = style

    constructor() : this("mocker.id", "mocker.name", "mocker.desc", BucketStyle(), BucketDates(), listOf())
}

Whose java decompilation looks like:

@PropertyName("bucket_style")
@NotNull
private final BucketStyle style;


@NotNull
public final BucketStyle getbucket_style() {
    return this.style;
}

@NotNull
public final BucketStyle getStyle() {
    return this.style;
}

Then the warning changes!

W/Firestore: (23.0.4) [CustomClassMapper]: No setter/field for style found on class com.[redacted].model.bucket.Bucket (fields/setters are case sensitive!)

I have no idea why. Also note that on the first warning, the property is referred to as bucket_style, and here as style.

If I do the same trick as before, renaming style to bucket_style, we get this:

@Parcelize
data class Bucket(
    @PropertyName("id")
    val id: String,

    @PropertyName("name")
    val name: String,
    @PropertyName("description")
    val description: String,

    @PropertyName("bucket_style")
    val bucket_style: BucketStyle,

    @PropertyName("bucket_dates")
    val dates: BucketDates
) : Parcelable {

    fun getbucket_style() = bucket_style

    constructor() : this("mock.id", "mock.name", "mock.desc", BucketStyle(), BucketDates(), listOf())
}

Whose java decompilation looks like

@NotNull
private final String description;

@PropertyName("bucket_style")
@NotNull
public final BucketStyle getbucket_style() {
    return this.bucket_style;
}

@NotNull
public final BucketStyle getBucket_style() {
    return this.bucket_style;
}

This crashes without warnings, because (I believe) there are two functions with the same, case-insensitive name:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.[redacted], PID: 18996
    java.lang.RuntimeException: Found conflicting getters for name getbucket_style on class com.[redacted].model.bucket.Bucket

Concolusions

The alternative would be to use kotlin Maps to model the nested firestore maps, but classes are much nicer to work with.
I also tried to un-parcele all the classes but the problem persists.
Is there a clean solution / something wrong with the implementation?



from Deserializing nested firebase firestore classes on Android

No comments:

Post a Comment