I am trying to use reified type when parsing json. It works perfectly with single json entry, but fails with list.
QUESTIONS:
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
}
}
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.
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)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With