others-how to solve `java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class xxx` when trying to run an kotlin app?
1. Purpose
In this post, I will demonstrate how to solve the error java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class xxx
when using kotlin to consume a springboot restful webservice.
The error
Here is the detailed error stacktrace of my error:
14:08:08.403 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://localhost:8081/msg/
14:08:08.437 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[application/json, application/*+json]
14:08:08.482 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
14:08:08.494 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.util.List<?>]
got 3 messages as follows:
Exception in thread "main" java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.example.domain.Message (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.example.domain.Message is in unnamed module of loader 'app')
at com.example.web.WebApplicationKt.test1(WebApplication.kt:22)
at com.example.web.WebApplicationKt.main(WebApplication.kt:14)
> Task :api-web:bootRun FAILED
Execution failed for task ':api-web:bootRun'.
> Process 'command '/Library/Java/JavaVirtualMachines/jdk-11.0.17.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
The JDK
We are using the JDK 11:
java 11
The codes that caused the error
The codes that caused the above error are as follows:
1) The rest controller that provide the restful service:
@RestController
class MessageController {
@GetMapping
fun index(): List<Message> {
var result = listOf(
Message("1","hello1"),
Message("2","Object"),
Message("3","Privot"),
)
return result
}
}
You can see that this is a very simple rest service that just return a list of objects when you send a http get request to the ‘/’ context.
After start the service, we can test the service via curl
:
➜ blog-backend-apis git:(master) ✗ curl http://localhost:8081/msg/
[{"id":"1","text":"hello1"},{"id":"2","text":"Object"},{"id":"3","text":"Privot"}]
You can see that the test is fine.
2) The consumer or client of the restful web service
import com.example.domain.Message
import org.springframework.web.client.RestTemplate
fun test1() {
val msgs:List<Message>? =
RestTemplate().getForObject("http://localhost:8081/msg/", List::class.java) as List<Message>
if(msgs!=null) {
println("got ${msgs.size} messages as follows:")
for (msg in msgs) { //this line cause the error
println(msg)
}
}
}
You can see that we just use the RestTemplate
to consume the webservice, and convert the result to List<Message>
.
Run the consumer test function, we got this error:
14:08:08.403 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://localhost:8081/msg/
14:08:08.437 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[application/json, application/*+json]
14:08:08.482 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
14:08:08.494 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.util.List<?>]
got 3 messages as follows:
Exception in thread "main" java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.example.domain.Message (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.example.domain.Message is in unnamed module of loader 'app')
at com.example.web.WebApplicationKt.test1(WebApplication.kt:22)
at com.example.web.WebApplicationKt.main(WebApplication.kt:14)
> Task :api-web:bootRun FAILED
Execution failed for task ':api-web:bootRun'.
> Process 'command '/Library/Java/JavaVirtualMachines/jdk-11.0.17.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
The core error message is:
Exception in thread "main" java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.example.domain.Message (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.example.domain.Message is in unnamed module of loader 'app')
at com.example.web.WebApplicationKt.test1(WebApplication.kt:22)
at com.example.web.WebApplicationKt.main(WebApplication.kt:14)
Debug the error
You can see that the following line caused the error:
And I add a breakpoint at this line to check the real type of msgs
, I got this:
You can see that the real data type of msgs
is an ArrayList , whose element’s type is LinkedHashMap
, not the expected Message
object. So the error happened.
What does this error mean?
I think this is an error that caused by the JSON reader(parser) used in the RestTemplate, by default, the springboot framework use the Jackson as the default JSON reader(parser).
Here we tried to convert json array to List<Message>
:
val msgs:List<Message>? =
RestTemplate().getForObject("http://localhost:8081/msg/", List::class.java) as List<Message>
But this does not work, because we can not specify the real type List<Message>
to getForObject()
.
Jackson reader tried to read the JSON, and then convert to a list, but the generic type is missing, it just use the LinkedHashMap
as the element type of the list.
Other useful information about this error
Here is an answer from stackoverflow:
The issue’s coming from Jackson. When it doesn’t have enough information on what class to deserialize to, it uses LinkedHashMap.
Since you’re not informing Jackson of the element type of your ArrayList, it doesn’t know that you want to deserialize into an ArrayList of Accounts. So it falls back to the default.
Instead, you could probably use as(JsonNode.class), and then deal with the ObjectMapper in a richer manner than rest-assured allows. Something like this:
2. Solution
Solution #1
We can just solve the problem by lazy converting the JSON to list, let’t do this job in two steps:
- First, get the json string
- Second, convert the json string to
List<Message>
Here is the code:
import com.example.domain.Message
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.web.client.RestTemplate
fun test() {}
val mapper = ObjectMapper() //jackson parser
val msgstr:String? =
RestTemplate().getForObject("http://localhost:8081/msg/", String::class.java) // get json string
val msgs: List<Message> = mapper.readValue(msgstr, object : TypeReference<List<Message>>() {}) //convert json string to List<Message>
if(msgs!=null) {
println("got ${msgs.size} messages as follows:")
for (msg in msgs) {
println(msg)
}
}
}
run the code, we got this:
11:08:17.698 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://localhost:8081/msg/
11:08:17.713 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*]
11:08:17.866 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
11:08:17.869 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json"
got 3 messages as follows:
Message(id=1, text=hello1)
Message(id=2, text=Object)
Message(id=3, text=Privot)
The above solution’s key point is to use the ObjectMapper
of jackson to convert a JSON string to a kotlin list of messages:
val msgs: List<Message> = mapper.readValue(msgstr, object : TypeReference<List<Message>>() {}) //convert json string to List<Message>
Pay attention to the parameter object : TypeReference<List<Message>>() {}
, it’s an anonymous object with type TypeReference<List<Message>>
. If you are java developers, you can understand this by reading the following example:
Anonymous object in java:
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Hello World");
}
};
thread.start();
Anonymous object in kotlin:
val thread = object : Thread() {
override fun run() {
println("Hello World")
}
}
thread.start()
Solution #2
For another working solution ,you can use the resttemplate’s getForEntity
instead of the getForObject
, the detailed steps are as follows:
- Use restTemplate’s
getForEntity
to get the restful service response - Get the
Array<Message>
object from the response - Get the
List<Message>
fromArray<Message>
Here is the code:
fun test_success_2() {
val responseEntity: ResponseEntity<Array<Message>> = RestTemplate().getForEntity("http://localhost:8081/msg/",
Array<Message>::class.java)
val msgsArray: Array<Message> = responseEntity.body!!
val msgs: List<Message> = Arrays.stream(msgsArray)
.collect(Collectors.toList())
if(msgs!=null) {
println("got ${msgs.size} messages as follows:")
for (msg in msgs) {
println(msg)
}
}
}
run the code, we get this:
11:29:10.500 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://localhost:8081/msg/
11:29:10.540 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[application/json, application/*+json]
11:29:10.569 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
11:29:10.578 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [com.example.domain.Message[]]
got 3 messages as follows:
Message(id=1, text=hello1)
Message(id=2, text=Object)
Message(id=3, text=Privot)
Process finished with exit code 0
Solution #3
For the third solution, we can use the RestTemplate’s exchange
method, which takes a ParameterizedTypeReference
as an parameter to specify the targeting type you want.
The steps are :
- Use RestTemplate’s
exchange
method to get the reponse, specifying the ParameterizedTypeReference to our type - Get
List<Message>
from the response body - Use the result
Here is the code:
fun test_success_3() {
val responseEntity: ResponseEntity<List<Message>> = RestTemplate().exchange(
"http://localhost:8081/msg/",
HttpMethod.GET,
null,
object : ParameterizedTypeReference<List<Message>>() {}
)
val msgs: List<Message>? = responseEntity.body
if(msgs!=null) {
println("got ${msgs.size} messages as follows:")
for (msg in msgs) {
println(msg)
}
}
}
You can see that the third solution is very concise and intuitive, the type conversion is happened inside the jackson reader.
3. The code example
The above codes has been uploaded to github, here is the link:
https://github.com/bswen/bswen-blog-backend-apis/blob/main/api-web/src/test/kotlin/tester.kt
4. Summary
In this post, I demonstrated how to solve the java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class xxx
when trying to run an kotlin restful client or consumer, the key point is to use the correct JSON to List converter to convert from the JSON to our List. That’s it, thanks for your reading.