1. 简介

Jackson 是 Java 中广泛使用的库,用于方便地处理 JSON 或 XML 的序列化与反序列化操作。

在将 JSON 或 XML 数据反序列化为对象集合时,我们可能会遇到如下异常:

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to X

本文将深入探讨该异常出现的原因,并提供几种有效的解决方案。

2. 问题分析

我们先通过一个简单的例子来复现这个异常,从而更好地理解它的触发条件。

2.1. 创建 POJO 类

我们定义一个简单的 Book 类:

public class Book {
    private Integer bookId;
    private String title;
    private String author;
    // 省略 getter、setter、构造函数、equals 和 hashCode
}

假设我们有一个 books.json 文件,内容如下,是一个包含三本书的 JSON 数组:

[ {
    "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"
} ]

接下来我们尝试将这个 JSON 反序列化为 List<Book>,看看会发生什么。

2.2. 将 JSON 反序列化为 List<Book>

我们尝试将 JSON 字符串反序列化为 List<Book> 并读取元素,看看是否会抛出类型转换异常:

@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.*");
}

测试通过,说明我们成功复现了问题。

2.3. 为什么会出现这个异常?

从异常信息 class java.util.LinkedHashMap cannot be cast to class … Book 可以看出几个问题:

  • 我们明明声明的是 List<Book>,为什么 Jackson 试图将 LinkedHashMap 转换为 Book
  • LinkedHashMap 是从哪来的?

根本原因如下:

  1. 我们调用 objectMapper.readValue(jsonString, ArrayList.class) 时,只告诉 Jackson 返回一个 ArrayList,但没有告诉它里面元素的类型。
  2. 因此 Jackson 会默认将每个 JSON 对象反序列化为 LinkedHashMap,最终得到的是 ArrayList<LinkedHashMap>
  3. 当我们试图调用 .getBookId() 时,就会抛出类型转换异常。

下图展示了反序列化后的结构:

booksMap

3. 使用 TypeReference 解决问题

为了解决这个问题,我们需要明确告诉 Jackson 集合中元素的类型。

推荐方式:使用 TypeReference

@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);
}

测试通过,说明 TypeReference 成功解决了类型擦除问题。

4. 使用 JavaType 解决问题

除了 TypeReference,我们还可以使用 Jackson 提供的 JavaType 来解决。

使用 JavaType 构造目标类型:

@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);
}

同样可以解决问题。

5. 使用 JsonNode + convertValue() 方法

另一种方式是先将 JSON 解析为 JsonNode,再通过 convertValue() 转换为目标类型。

使用 convertValue() 配合 TypeReferenceJavaType

@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);
}

或者使用 JavaType

@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);
}

两种方式都 ✅ 通过测试。

6. 编写通用的反序列化方法

在实际开发中,我们可能需要一个通用的方法来处理不同类型的集合。

使用 JavaType 实现通用方法:

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);
}

测试方法:

@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);
}

测试 ✅ 通过。

⚠️ 注意:不能使用 TypeReference 来实现通用方法!

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

虽然代码看起来没问题,但运行时会抛出同样的异常:

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

原因在于泛型擦除,TypeReference<List<T>> 中的 T 在运行时是无法获取的。

7. XML 反序列化中的相同问题

Jackson 不仅支持 JSON,也支持 XML 的序列化与反序列化。

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>

测试代码:

@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.*");
}

测试 ✅ 通过,说明问题同样存在。

解决方式与 JSON 一致:

@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);
}

因为 XmlMapperObjectMapper 的子类,所以所有 JSON 的解决方案都适用于 XML。

8. 总结

本文详细解释了 Jackson 在反序列化 JSON 或 XML 为集合时,为什么会抛出 LinkedHashMap cannot be cast to X 的异常,并提供了以下几种解决方案:

推荐方式:

  • 使用 TypeReference 明确指定泛型类型
  • 使用 JavaType 构造目标类型
  • 使用 JsonNode + convertValue() 进行类型转换

⚠️ 踩坑提醒:

  • 泛型方法中不能使用 TypeReference<List<T>>,因为泛型擦除
  • XML 和 JSON 的反序列化机制一致,解决方案通用

所有代码示例均可在 GitHub 获取。


原始标题:Jackson: java.util.LinkedHashMap cannot be cast to X