GitLiveApp / firebase-kotlin-sdk

A Kotlin-first SDK for Firebase
https://gitliveapp.github.io/firebase-kotlin-sdk/
Apache License 2.0
1.09k stars 154 forks source link

ClassCastException when deserialising sealed class from Firestore in KotlinJS #343

Closed gchristov closed 2 months ago

gchristov commented 1 year ago

I'm trying to serialise/deserialise the following model into Firestore on KotlinJS with nodeJs() target, which is correctly written to Firestore, along with its generated type.

@Serializable
data class ApiSearchSession(
    val id: String,
    val state: ApiState
)

@Serializable
sealed class ApiState {
    @Serializable
    object Searching : ApiState()
}

I'm writing this to Firestore like this

val document = firebaseFirestore
            .collection("COLLECTION")
            .document(searchSession.id)
        document.set(
            data = searchSession.toApiSearchSession(),
            encodeDefaults = true
        )

However, when deserialising I'm getting a ClassCastException. My deserialisation code is

val document = firebaseFirestore
            .collection("searchSession")
            .document(id)
            .get()
        return if (document.exists) {
            val apiSearchSession: ApiSearchSession = document.data()
            apiSearchSession.toSearchSession()
        } else {
            null
        }

Am I missing something? I've tried serialising the above to/from Strings with kotlinx.serialisation and it works as it should which lead me to think it's an SDK issue here. I'm on version 1.6.2 of the SDK.

Thanks in advance! 🙏

gchristov commented 1 year ago

Also, worth noting that normal object serialisation works. I'm seeing the issue with sealed classes only.

gchristov commented 1 year ago

As a workaround I've added a custom serializer which converts to/from String.

private object SessionStateSerializer : KSerializer<ApiSearchSession.State> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
        serialName = "ApiSearchSession.State",
        kind = PrimitiveKind.STRING
    )

    override fun deserialize(decoder: Decoder): ApiSearchSession.State {
        return Json.decodeFromString(decoder.decodeString())
    }

    override fun serialize(
        encoder: Encoder,
        value: ApiSearchSession.State
    ) {
        encoder.encodeString(Json.encodeToString(value))
    }
}

and am using it on the property directly like

@Serializable(with = SessionStateSerializer::class)
val state: State