Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin. Trying to use reified types to parse Lists and Arrays

I am trying to use reified type when parsing json. It works perfectly with single json entry, but fails with list.

QUESTIONS:

  1. What am I missing in String.parseList() method?
  2. How come ClassCastException upon .first() despite assignment passed one line earlier?
    package qa

    import com.fasterxml.jackson.databind.ObjectMapper
    import org.slf4j.LoggerFactory
    import org.testng.Assert
    import org.testng.annotations.Test

    class ReifiedParseListTest {

        data class User(var name: String = "userName", var age: Int = 0)

        val log = LoggerFactory.getLogger(this.javaClass.name)
        val objectMapper = ObjectMapper()
        val json: String = """[{"name":"Alice","age":1},{"name":"Bob","age":2}]"""
        val expected: String = "[User(name=Alice, age=1), User(name=Bob, age=2)]"


        inline fun <reified V> String.parseList(): List<V> = objectMapper
                .readValue(this, Array<V>::class.java).toList()


        @Test
        fun checkParseList_OK() {
            val actual: List<User> = objectMapper
                    .readValue(json, Array<User>::class.java).toList()

            log.info("actual.first() is of type: {}", actual.first().javaClass)
            Assert.assertEquals(actual.toString(), expected)
        }

        @Test
        fun checkParseListReified_FAILS() {
            val actual: List<User> = json.parseList<User>()
            Assert.assertEquals(actual.toString(), expected)
            // java.lang.AssertionError:
            // Expected :[User(name=Alice, age=1), User(name=Bob, age=2)]
            // Actual   :[{name=Alice, age=1}, {name=Bob, age=2}]
        }

        @Test
        fun checkParseListReifiedClassCast_FAILS() {
            val actual: List<User> = json.parseList<User>()
            log.info("actual.first() is of type: {}", actual.first().javaClass)
            // java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to qa.ReifiedParseListTest$User
        }

    }
like image 953
ludenus Avatar asked Oct 27 '25 07:10

ludenus


2 Answers

In this case, reified helps to propagate the type's class, but there's still type erasure.
To avoid that, you can use something like JavaType:

inline fun <reified V> String.parseList(): List<V> {
    return objectMapper.readValue(this, objectMapper.getTypeFactory()
        .constructCollectionType(List::class.java, V::class.java))
}

Note that without reified we wouldn't be able to use V::class.java

Now to answer your second question, how come that although val actual is List<User>, you get ClassCastException - the answer is again type erasure, with some obfuscation of platform types.

If you look at what this function returns (it's your function without asList() call:

inline fun <reified V> String.parseList() = 
        objectMapper.readValue(this, Array<V>::class.java)

You'll notice it returns Array<???>!, which is Kotlin's way of saying "it's something from Java, I hope it will work, but I can't promise". Now by calling toList() this relaxes the compiler, saying "yeah, in the end we return a Kotlin type, it will be alright". But that's a false promise, actually.

What you get is Array<Any> filled with LinkedHashMap, which of course fail when they're being cast to User based on a false promise we've given the compiler.

like image 167
Alexey Soshin Avatar answered Oct 29 '25 06:10

Alexey Soshin


i finally end up with yet another solution, that seems to handle both single entities and lists

    inline fun <reified V> String.parse(): V = objectMapper.readValue(this, object : TypeReference<V>() {})

@Test
fun checkParseSingle() {
    val jsonSingle: String = """{"name":"Carol","age":3}"""
    val expectedSingle: String = "User(name=Carol, age=3)"

    val actual: User = jsonSingle.parse<User>()
    Assert.assertEquals(actual.toString(), expectedSingle)
}

@Test
fun checkParseList() {
    val jsonList: String = """[{"name":"Alice","age":1},{"name":"Bob","age":2}]"""
    val expectedList: String = "[User(name=Alice, age=1), User(name=Bob, age=2)]"

    val actual: List<User> = jsonList.parse<List<User>>()
    Assert.assertEquals(actual.toString(), expectedList)
}
like image 22
ludenus Avatar answered Oct 29 '25 06:10

ludenus



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!