Jackson Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course (COVID-pricing ends in January):

>> CHECK OUT THE COURSE

1. Overview

Jackson is a widely used Java library, which allows us to serialize/deserialize JSON or XML conveniently.

Sometimes, we may encounter “java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to X” when we try to deserialize JSON or XML into a collection of objects.

In this tutorial, we'll discuss why the mentioned exception can occur and how to solve the problem.

2. Understanding the Problem

Let's create a simple Java application to reproduce this exception to understand when the exception will occur.

2.1. Creating a POJO Class

Let's start with a simple POJO class:

public class Book {
    private Integer bookId;
    private String title;
    private String author;
    //getters, setters, constructors, equals and hashcode omitted
}

Now, suppose we have the books.json file consisting of a JSON array that contains three books:

[ {
    "bookId" : 1,
    "title" : "A Song of Ice and Fire",
    "author" : "George R. R. Martin"
}, {
    "bookId" : 2,
    "title" : "The Hitchhiker's Guide to the Galaxy",
    "author" : "Douglas Adams"
}, {
    "bookId" : 3,
    "title" : "Hackers And Painters",
    "author" : "Paul Graham"
} ]

Next, we'll see what happens when we try to deserialize our JSON example to List<Book>:

2.2. Deserializing JSON to List<Book>

Let's see if we can reproduce the class casting problem by deserializing this JSON file to a List<Book> object and reading the elements from it:

@Test
void givenJsonString_whenDeserializingToList_thenThrowingClassCastException() 
  throws JsonProcessingException {
    String jsonString = readFile("/to-java-collection/books.json");
    List<Book> bookList = objectMapper.readValue(jsonString, ArrayList.class);
    assertThat(bookList).size().isEqualTo(3);
    assertThatExceptionOfType(ClassCastException.class)
      .isThrownBy(() -> bookList.get(0).getBookId())
      .withMessageMatching(".*java.util.LinkedHashMap cannot be cast to .*com.baeldung.jackson.tocollection.Book.*");
}

We've used the AssertJ library to verify that the expected exception is thrown when we call bookList.get(0).getBookId() and that its message matches the one noted in our problem statement.

The test passes, meaning that we've reproduced the problem successfully.

2.3. Why the Exception Is Thrown

Now, if we take a closer look at the exception message: “class java.util.LinkedHashMap cannot be cast to class … Book“, a couple of questions may come up.

We've declared the variable bookList with the type List<Book>, but why does Jackson try to cast the LinkedHashMap type to our Book class? Furthermore, where does the LinkedHashMap come from?

First, indeed we declared bookList with the type List<Book>. However, when we called the objectMapper.readValue() method, we passed ArrayList.class as the Class object. Therefore, Jackson will deserialize the JSON content to an ArrayList object, but it has no idea what type of elements should be in the ArrayList object.

Second, when Jackson attempts to deserialize an object in JSON, but no target type information is given, it'll use the default type: LinkedHashMap. In other words, after the deserialization, we'll get an ArrayList<LinkedHashMap> object. In the Map, the keys are the names of the properties — for example, “bookId“, “title“, and so on. The values are the values of the corresponding properties:

Now that we understand the cause of the problem, let's discuss how to solve it.

3. Passing TypeReference to objectMapper.readValue()

To solve the problem, we need to somehow let Jackson know the type of the element. However, the compiler doesn't allow us to do something like objectMapper.readValue(jsonString, ArrayList<Book>.class).

Instead, we can pass a TypeReference object to the objectMapper.readValue(String content, TypeReference valueTypeRef) method. In this case, we just need to pass new TypeReference<List<Book>>() {} as the second parameter:

@Test
void givenJsonString_whenDeserializingWithTypeReference_thenGetExpectedList() 
  throws JsonProcessingException {
    String jsonString = readFile("/to-java-collection/books.json");
    List<Book> bookList = objectMapper.readValue(jsonString, new TypeReference<List<Book>>() {});
    assertThat(bookList.get(0)).isInstanceOf(Book.class);
    assertThat(bookList).isEqualTo(expectedBookList);
}

If we run the test, it'll pass. So, passing a TypeReference object solves our problem.

4. Passing JavaType to objectMapper.readValue()

In the previous section, we talked about passing a Class object or a TypeReference object as the second parameter to call the objectMapper.readValue() method.

The objectMapper.readValue() method still accepts a JavaType object as the second parameter. The JavaType is the base class of type-token classes. It'll be used by the deserializer so that the deserializer knows what the target type is during the deserialization. 

We can construct a JavaType object through a TypeFactory instance, and we can retrieve the TypeFactory object from objectMapper.getTypeFactory().

Let's come back to our book example. In this example, the target type we want to have is ArrayList<Book>. Therefore, we can construct a CollectionType with this requirement:

objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, Book.class);

Now, let's write a unit test to see if passing a JavaType to the readValue() method will solve our problem:

@Test
void givenJsonString_whenDeserializingWithJavaType_thenGetExpectedList() 
  throws JsonProcessingException {
    String jsonString = readFile("/to-java-collection/books.json");
    CollectionType listType = 
      objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, Book.class);
    List<Book> bookList = objectMapper.readValue(jsonString, listType);
    assertThat(bookList.get(0)).isInstanceOf(Book.class);
    assertThat(bookList).isEqualTo(expectedBookList);
}

The test passes if we run it. Therefore, the problem can be solved in this way, as well.

5. Using the JsonNode Object and the objectMapper.convertValue() Method

We've seen the solution of passing a TypeReference or JavaType object to the objectMapper.readValue() method.

Alternatively, we can work with tree model nodes in Jackson and then convert the JsonNode object into the desired type by calling the objectMapper.convertValue() method.

Similarly, we can pass an object of TypeReference or JavaType to the objectMapper.convertValue() method.

Let's see each approach in action.

First, let's create a test method using a TypeReference object and the objectMapper.convertValue() method:

@Test
void givenJsonString_whenDeserializingWithConvertValueAndTypeReference_thenGetExpectedList() 
  throws JsonProcessingException {
    String jsonString = readFile("/to-java-collection/books.json");
    JsonNode jsonNode = objectMapper.readTree(jsonString);
    List<Book> bookList = objectMapper.convertValue(jsonNode, new TypeReference<List<Book>>() {});
    assertThat(bookList.get(0)).isInstanceOf(Book.class);
    assertThat(bookList).isEqualTo(expectedBookList);
}

Now, let's see what happens when we pass a JavaType object to the objectMapper.convertValue() method:

@Test
void givenJsonString_whenDeserializingWithConvertValueAndJavaType_thenGetExpectedList() 
  throws JsonProcessingException {
    String jsonString = readFile("/to-java-collection/books.json");
    JsonNode jsonNode = objectMapper.readTree(jsonString);
    List<Book> bookList = objectMapper.convertValue(jsonNode, 
      objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, Book.class));
    assertThat(bookList.get(0)).isInstanceOf(Book.class);
    assertThat(bookList).isEqualTo(expectedBookList);
}

If we run the two tests, both of them will pass. Therefore, using the objectMapper.convertValue() method is an alternative way to solve the problem.

6. Creating a Generic Deserialization Method

So far, we've addressed how to solve the class casting problem when we deserialize JSON array to Java collections. In the real world, we may want to create a generic method to handle different element types.

It won't be a hard job for us now. We can pass a JavaType object when calling the objectMapper.readValue() method:

public static <T> List<T> jsonArrayToList(String json, Class<T> elementClass) throws IOException {
    ObjectMapper objectMapper = new ObjectMapper();
    CollectionType listType = 
      objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, elementClass);
    return objectMapper.readValue(json, listType);
}

Next, let's create a unit-test method to verify if it works as we expect:

@Test
void givenJsonString_whenCalljsonArrayToList_thenGetExpectedList() throws IOException {
    String jsonString = readFile("/to-java-collection/books.json");
    List<Book> bookList = JsonToCollectionUtil.jsonArrayToList(jsonString, Book.class);
    assertThat(bookList.get(0)).isInstanceOf(Book.class);
    assertThat(bookList).isEqualTo(expectedBookList);
}

The test will pass if we run it.

Why not use the TypeReference approach to build the generic method since it looks more compact?

Now, let's create a generic utility method and pass the corresponding TypeReference object to the objectMapper.readValue() method:

public static <T> List<T> jsonArrayToList(String json, Class<T> elementClass) throws IOException {
    return new ObjectMapper().readValue(json, new TypeReference<List<T>>() {});
}

The method looks straightforward. If we run the test method once again, we'll get:

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.baeldung...Book ...

Oops, an exception occurred!

We've passed a TypeReference object to the readValue() method, and we've previously seen that this way will solve the class casting problem. So, why are we seeing the same exception in this case?

It's because our method is generic. The type parameter T cannot be reified at runtime, even if we pass a TypeReference instance with the type parameter T.

7. XML Deserialization With Jackson

Apart from JSON serialization/deserialization, the Jackson library can be used to serialize/deserialize XML as well.

Let's make a quick example to check if the same problem can happen when deserializing XML to Java collections.

First, let's create an XML file books.xml:

<ArrayList>
    <item>
        <bookId>1</bookId>
        <title>A Song of Ice and Fire</title>
        <author>George R. R. Martin</author>
    </item>
    <item>
        <bookId>2</bookId>
        <title>The Hitchhiker's Guide to the Galaxy</title>
        <author>Douglas Adams</author>
    </item>
    <item>
        <bookId>3</bookId>
        <title>Hackers And Painters</title>
        <author>Paul Graham</author>
    </item>
</ArrayList>

Next, as what we've done with the JSON file, we create another unit-test method to verify if the class casting exception will be thrown:

@Test
void givenXml_whenDeserializingToList_thenThrowingClassCastException() 
  throws JsonProcessingException {
    String xml = readFile("/to-java-collection/books.xml");
    List<Book> bookList = xmlMapper.readValue(xml, ArrayList.class);
    assertThat(bookList).size().isEqualTo(3);
    assertThatExceptionOfType(ClassCastException.class)
      .isThrownBy(() -> bookList.get(0).getBookId())
      .withMessageMatching(".*java.util.LinkedHashMap cannot be cast to .*com.baeldung.jackson.tocollection.Book.*");
}

Our test will pass if we give it a run. That is to say, the same problem occurs in XML deserialization as well.

However, if we know how to solve JSON deserialization, it's pretty simple to fix it in XML deserialization.

Since XmlMapper is a subclass of ObjectMapper, all the solutions we've addressed for JSON deserialization also work for XML deserialization.

For example, we can pass a TypeReference object to the xmlMapper.readValue() method to solve the problem:

@Test
void givenXml_whenDeserializingWithTypeReference_thenGetExpectedList() 
  throws JsonProcessingException {
    String xml = readFile("/to-java-collection/books.xml");
    List<Book> bookList = xmlMapper.readValue(xml, new TypeReference<List<Book>>() {});
    assertThat(bookList.get(0)).isInstanceOf(Book.class);
    assertThat(bookList).isEqualTo(expectedBookList);
}

8. Conclusion

In this article, we've discussed why we may get “java.util.LinkedHashMap cannot be cast to X” exception when we use Jackson to deserialize JSON or XML.

After that, we've addressed different ways to solve the problem through examples.

As always, the code in this write-up is all available over on GitHub.

Jackson bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course (COVID-pricing ends in January):

>> CHECK OUT THE COURSE
guest
0 Comments
Inline Feedbacks
View all comments