google / gson

A Java serialization/deserialization library to convert Java Objects into JSON and back
Apache License 2.0
23.4k stars 4.29k forks source link

Gson Conversion Issue: Unable to Convert java.util.ArrayList to RealmList Implicitly During Deserialization in Android Studio #2730

Open chaitanya-anand opened 2 months ago

chaitanya-anand commented 2 months ago

Gson version

2.10.1 AND 2.11.0

Java / Android version

Android version 13

Used tools

Stack Trace

java.lang.IllegalArgumentException: field com.nextgenpos.nextgenserverpos.data.model.order.nestedOrderModels.Item.customisedAddOns has type io.realm.kotlin.types.RealmList, got java.util.ArrayList
2024-08-29 15:58:53.232 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at java.lang.reflect.Field.set(Native Method)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$2.readIntoField(ReflectiveTypeAdapterFactory.java:278)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$FieldReflectionAdapter.readField(ReflectiveTypeAdapterFactory.java:558)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:516)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.read(TypeAdapterRuntimeTypeWrapper.java:40)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:83)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:59)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$2.readIntoField(ReflectiveTypeAdapterFactory.java:267)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$FieldReflectionAdapter.readField(ReflectiveTypeAdapterFactory.java:558)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:516)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.Gson.fromJson(Gson.java:1361)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.Gson.fromJson(Gson.java:1262)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.Gson.fromJson(Gson.java:1171)
2024-08-29 15:58:53.233 27746-28472 System.err              com.nextgenpos.nextgenserverpos      W      at com.google.gson.Gson.fromJson(Gson.java:1107)

Description

When deserialising a JSON into an object containing RealmList Gson is unable to populate it correctly and throws the following error. In fact the same error occurs in case of RealmDictionary too.

Expected behavior

The deserialised JSON should correctly populate the any list type to RealmList

Actual behaviour

IllegalArgumentException is thrown

Marcono1234 commented 2 months ago

If possible, could you please create a small self-contained example or demo project? Otherwise it is difficult to reproduce this or to find the cause. For example maybe there is some non-type-safe code involved which causes the incorrect deserialized class.

Could be related to / same issue as #419 or #1708 though, for example here a custom List subtype MyList also causes a ClassCastException:

interface MyList<T> extends List<T> {}
public static void main(String[] args) {
  var listType = new TypeToken<MyList<String>>() {};
  MyList<String> list = new Gson().fromJson("[]", listType);
}
sonuindori commented 2 months ago

Same happening for RealmDictionary field com.example.refactor.models.dbmodels.Form.collaborators has type io.realm.kotlin.types.RealmDictionary, got java.util.LinkedHashMap

When changing it to RealmList error converts to has type io.realm.kotlin.types.RealmList, got java.util.ArrayList

chaitanya-anand commented 2 months ago

Hi sorry for the late reply.

I have created a demo android project to showcase the error:

MainActivity

package com.jubl.realmgsontest

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.jubl.realmgsontest.databinding.ActivityMainBinding
import io.realm.kotlin.ext.realmListOf
import io.realm.kotlin.types.RealmList

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.btn1.setOnClickListener {
            val jsonString = serializeJson()
            binding.text1.text = jsonString
            deserializeJson(jsonString)
        }
    }

    private fun serializeJson(): String {
        val modelB: RealmList<ModelB> = realmListOf(
            ModelB().apply { internalName = "Alice"
                internalId = "B1"},
            ModelB().apply { internalName = "Bob"
                internalId = "B2"},
            ModelB().apply { internalName = "Cat"
                internalId = "B3"}
        )

        val modelA: ModelA = ModelA().apply {
            id = "A1"
            name = "ModelAName"
            listCourses = realmListOf("CS","IT","ECE")
            listModelB = modelB
        }

        val gson = Gson()
        val toJson = gson.toJson(modelA)
        println(toJson)
        return toJson
    }

    private fun deserializeJson(json: String) {
        val gson = GsonBuilder().serializeNulls().create()
        val model = gson.fromJson(json, ModelA::class.java)
        println(model)
    }
}

with the following model:

ModelA and ModelB:

package com.jubl.realmgsontest

import io.realm.kotlin.ext.realmListOf
import io.realm.kotlin.types.EmbeddedRealmObject
import io.realm.kotlin.types.RealmList
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey

class ModelA: RealmObject {

    @PrimaryKey
    var id: String = ""
    var name: String = ""

    var listCourses: RealmList<String> = realmListOf()
    var listModelB : RealmList<ModelB> = realmListOf()
}

class ModelB: EmbeddedRealmObject {

    var internalName: String = ""
    var internalId: String = ""
}

Gson breaks with the following stack trace when the deserializeJson() function runs:

FATAL EXCEPTION: main
                                                                                                    Process: com.jubl.realmgsontest, PID: 19640
                                                                                                    java.lang.IllegalArgumentException: field com.jubl.realmgsontest.ModelA.listCourses has type io.realm.kotlin.types.RealmList, got java.util.ArrayList
                                                                                                        at java.lang.reflect.Field.set(Native Method)
                                                                                                        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.readIntoField(ReflectiveTypeAdapterFactory.java:222)
                                                                                                        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$FieldReflectionAdapter.readField(ReflectiveTypeAdapterFactory.java:433)
                                                                                                        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:393)
                                                                                                        at com.google.gson.Gson.fromJson(Gson.java:1227)
                                                                                                        at com.google.gson.Gson.fromJson(Gson.java:1137)
                                                                                                        at com.google.gson.Gson.fromJson(Gson.java:1047)
                                                                                                        at com.google.gson.Gson.fromJson(Gson.java:982)
                                                                                                        at com.jubl.realmgsontest.MainActivity.deserializeJson(MainActivity.kt:54)
                                                                                                        at com.jubl.realmgsontest.MainActivity.onCreate$lambda$0(MainActivity.kt:24)
                                                                                                        at com.jubl.realmgsontest.MainActivity.$r8$lambda$XNagGdFbnmIcx7WnNlWYg20JNeM(Unknown Source:0)
                                                                                                        at com.jubl.realmgsontest.MainActivity$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
                                                                                                        at android.view.View.performClick(View.java:7659)
                                                                                                        at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1218)
                                                                                                        at android.view.View.performClickInternal(View.java:7636)
                                                                                                        at android.view.View.-$$Nest$mperformClickInternal(Unknown Source:0)
                                                                                                        at android.view.View$PerformClick.run(View.java:30155)
                                                                                                        at android.os.Handler.handleCallback(Handler.java:958)
                                                                                                        at android.os.Handler.dispatchMessage(Handler.java:99)
                                                                                                        at android.os.Looper.loopOnce(Looper.java:205)
                                                                                                        at android.os.Looper.loop(Looper.java:294)
                                                                                                        at android.app.ActivityThread.main(ActivityThread.java:8176)
                                                                                                        at java.lang.reflect.Method.invoke(Native Method)
                                                                                                        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
                                                                                                        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)

and as @sonuindori commented, the same error occurs when using RealmDictionary too.

For context here are the related dependencies I am using:

    id 'io.realm.kotlin' version '1.16.0' apply false
    implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1'

Let me know if you need any more info.

Marcono1234 commented 2 months ago

Thanks a lot for this detailed information!

I think even if this specific IllegalArgumentException was fixed, Gson would still fail with a different expected exception: The issue is that you are trying to deserialize an interface, and Gson cannot know which specific implementation of that interface you actually want to deserialize.

To solve this, you need to register a custom InstanceCreator which creates an instance of RealmList, so that Gson can use that for deserializing the values. Alternatively you can also register a TypeAdapter or JsonDeserializer, but that is probably not needed here. I am not familiar with the realm-kotlin library, but it seems you can probably use realmListOf() and realmDictionaryOf() within those InstanceCreators. Please let me know if something regarding this is unclear.

Note however that as mentioned in Gson's README, support for Kotlin and other JVM languages is limited. It seems realm-kotlin provides a io.realm.kotlin.serializers package. Maybe that would be more helpful?

In case my suggestion to use an InstanceCreator does not help, I will try your code snippet.

chaitanya-anand commented 2 months ago

I tried using InstanceCreator, however I am still getting the same error. Am I doing something wrong?

My InstanceCreator

package com.jubl.realmgsontest

import com.google.gson.InstanceCreator
import io.realm.kotlin.ext.realmListOf
import java.lang.reflect.Type

class ModelACreator: InstanceCreator<ModelA> {
    override fun createInstance(type: Type?): ModelA {
        val model: ModelA = ModelA().apply {
            listCourses = realmListOf()
            listModelB = realmListOf()
        }

//        model.listCourses = realmListOf<String>()
//        model.listModelB = realmListOf<ModelB>()
        return model
    }
}

Changed the deserializeJson() function to:

private fun deserializeJson(json: String) {
        val gson = GsonBuilder()
            .registerTypeAdapter(ModelA::class.java, ModelACreator())
            .create()

        val model = gson.fromJson(json, ModelA::class.java)
        println(model)
    }

From what I could understand, this error is occurring because Gson is deserialising the listCourses into a Java or Kotlin ArrayList or List and then setting the realmList in ModelA like this: modelA.listCourses = deserialisedList<String>. However this is not supported by realmList. (Basically you cannot do something like this: val rl: RealmList<String> = listOf("Apple", "Mango").

However we can do this:

val rl: RealmList<String> = realmListOf()
rl.addAll(deserialisedList)

So if somehow we can tell Gson to use the addAll function, then I think we can solve this issue.

Marcono1234 commented 2 months ago

What I meant was registering InstanceCreators for RealmList and RealmDictionary (since those are the types which Gson cannot deserialize on its own):

val gson = GsonBuilder()
    .registerTypeAdapter(RealmList::class.java, InstanceCreator {
        realmListOf<Any>()
    })
    .registerTypeAdapter(RealmDictionary::class.java, InstanceCreator {
        realmDictionaryOf<Any>()
    })
    .create()

(Same probably also for RealmSet if you use that too.)

At least in my local tests that seems to solve the issue.

chaitanya-anand commented 2 months ago

Thank you for your help. This worked perfectly.