Search code examples
arraysjsonkotlingsonclasscastexception

Why does Gson allow normal array declaration, then throw ClassCastException when accessing it in a Map?


When Gson is used to convert a JSON map to a Kotlin/Java Map, and the JSON values contain arrays as "[]", there will be a ClassCastException error if Array<T> is declared and then accessed. (Array<T> is equivalent to Java array T[]):

app/build.gradle:

dependencies {
   ...
   implementation 'com.google.code.gson:gson:2.8.6'
}

JsonConverter.kt (using this guide):

@Throws(JsonSyntaxException::class)
fun gsonMapTest() {
    val jsonInput = """
        {"keyOne":[],"keyTwo":["valueA"],"keyThree":[]}
    """.trimIndent()

    // Guidance: https://www.baeldung.com/gson-json-to-map
    val jsonMap1 = Gson().fromJson<LinkedTreeMap<String, Array<String>>>(jsonInput, MutableMap::class.java)
    val jsonMap2: Map<*,*> = Gson().fromJson(jsonInput, Map::class.java) // MutableMap can also be used

    // These work fine
    val firstKeyInMap1: String = jsonMap1.keys.first() // returns String
    val firstKeyInMap2 = jsonMap2.keys.first() // returns String

    // Errors here: "java.lang.ClassCastException: java.util.ArrayList cannot be cast to [Ljava.lang.String;"
    val firstValueInMap1: Array<String> = jsonMap1.values.first()
    val firstValueInMap2: Array<String> = jsonMap2.values.first() as Array<String>
}

This error happens at runtime:

java.lang.ClassCastException: java.util.ArrayList cannot be cast to [Ljava.lang.String;

But the Map objects are allowed to be created, without any warning or error. It's only when a Map value is accessed that the error occurs.

From the error message, I initially thought that the call to jsonMap1.values.first() was trying to cast the empty array to a String. I eventually figured out that the "[L" in the error message means "array of Objects"

If the declarations in the code sample are changed from Array<String> to ArrayList<String> then it all works fine.

So my questions are:

  • Is it normal behavior for Gson to allow Array<T> declaration, but cause an error when the item is accessed?
  • Is there a way to show a warning or compile error to say that ArrayList<String> should be used instead of Array<String>?

I checked the Github Issues for Gson. I couldn't find this issue reported, but I found some slightly similar ones:

Update: Perhaps this is the same problem?: Java gson, ClassCastException on decoding to Map<String,String[]>


Solution

  • As mentioned by user fluffy in his/her comments, one must use a TypeToken to deserialize a parameterized Generics type, in addition to the Generics delcaration. However, it does not seem to be needed for ArrayList<String>:

    @Test
    @Throws(JsonSyntaxException::class)
    fun gsonMapTest() {
        val jsonInput = """
        {"keyOne":[],"keyTwo":["valueA"],"keyThree":[]}
        """.trimIndent()
    
        // Guidance: https://www.baeldung.com/gson-json-to-map
        val jsonMap1 = Gson().fromJson<LinkedTreeMap<String, ArrayList<String>>>(jsonInput, MutableMap::class.java)
    
        val listType = object : TypeToken<LinkedTreeMap<String, Array<String>>>() {}.type
        val jsonMap2: Map<*,*> = Gson().fromJson(jsonInput, listType) // MutableMap can also be used
    
        // These work fine
        val firstKeyInMap1: String = jsonMap1.keys.first() // returns String "keyOne"
        val firstKeyInMap2 = jsonMap2.keys.first() // returns String "keyOne"
    
        // No longer errors here
        val firstValueInMap1: ArrayList<String> = jsonMap1.values.first() // returns empty list
        val firstValueInMap2: Array<String> = jsonMap2.values.first() as Array<String> // returns empty array
    }