Jackson:java.util.LinkedHashMap無法轉換為X

    1.概述

    Jackson是一個廣泛使用的Java庫,它使我們可以方便地對JSON或XML進行序列化/反序列化。

    有時,當我們嘗試將JSON或XML反序列化為對象集合時,我們可能會遇到“ java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to X ”。

    在本教程中,我們將討論為什麼會出現上述異常以及如何解決該問題。

    2.了解問題

    讓我們創建一個簡單的Java應用程序來重現此異常,以了解何時會發生該異常。

    2.1。創建一個POJO類

    讓我們從一個簡單的POJO類開始:

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

    現在,假設我們有一個books.json文件,該文件由一個包含三本書的JSON數組組成:

    [ {
    
     "bookId" : 1,
    
     "title" : "A Song of Ice and Fire",
    
     "author" : "George RR 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.*");
    
     }

    我們已經使用AssertJ庫來驗證在調用bookList.get(0).getBookId()時是否引發了預期的異常,並且其消息與問題語句中指出的消息相匹配.

    測試通過,表示我們已成功重現了該問題。

    2.3。為什麼引發異常

    現在,如果我們仔細看一下異常消息:“ class java.util.LinkedHashMap cannot be cast to class … Book ”,則可能會出現兩個問題。

    我們已經聲明了類型為List<Book>的變量bookList ,但是為什麼Jackson嘗試將LinkedHashMap類型轉換為Book類呢?此外, LinkedHashMap來自哪裡?

    首先,實際上我們聲明了類型為List<Book>. bookList List<Book>.但是,當我們調用objectMapper.readValue()方法時,我們將ArrayList.class傳遞為Class對象。因此,Jackson會將JSON內容反序列化為ArrayList對象,但不知道ArrayList像中應包含哪種類型的元素。

    其次,當Jackson嘗試在JSON中反序列化對象但未給出目標類型信息時,它將使用默認類型: LinkedHashMap 。換句話說,反序列化之後,我們將獲得ArrayList<LinkedHashMap>對象。在Map ,鍵是屬性的名稱,例如,“ bookId ”,“ title ”等。這些值是相應屬性的值:

    傑克遜:java.util.LinkedHashMap無法轉換為X

    現在我們了解了問題的原因,讓我們討論如何解決它。

    3.傳遞TypeReferenceobjectMapper.readValue()

    要解決該問題,我們需要以某種方式讓Jackson知道元素的類型。但是,編譯器不允許我們執行諸如objectMapper.readValue(jsonString, ArrayList<Book>.class)

    相反,我們可以將TypeReferenceobjectMapper.readValue(String content, TypeReference valueTypeRef)傳遞給objectMapper.readValue(String content, TypeReference valueTypeRef)方法。在這種情況下,我們只需要傳遞new TypeReference<List<Book>>() {}作為第二個參數:

    @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.傳JavaTypeobjectMapper.readValue()

    在上一節中,我們討論了傳遞Class像或TypeReference像作為第二個參數來調用objectMapper.readValue()方法。

    objectMapper.readValue()方法仍然接受JavaType像作為第二個參數。 JavaType是類型標記類的基類。反序列化器將使用它,以便反序列化器知道反序列化過程中的目標類型。

    我們可以通過TypeFactory實例構造一個JavaType對象,並且可以從objectMapper.getTypeFactory()檢索TypeFactory對象。

    讓我們回到我們的書示例中。在此示例中,我們要具有的目標類型是ArrayList<Book> 。因此,我們可以按照以下要求構造一個CollectionType

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

    現在,讓我們編寫一個單元測試,看看將JavaType傳遞給readValue()方法是否可以解決我們的問題:

    @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對象和objectMapper.convertValue()方法

    我們已經看到了將TypeReferenceJavaType對像傳遞給objectMapper.readValue()方法的解決方案。

    另外,我們可以使用Jackson中的樹模型節點,然後通過調用objectMapper.convertValue()方法將JsonNode對象轉換為所需的類型。

    類似地,我們可以將TypeReferenceJavaTypeobjectMapper.convertValue() JavaTypeobjectMapper.convertValue()方法。

    讓我們看看每種方法的實際作用。

    首先,讓我們使用TypeReference對象和objectMapper.convertValue()方法創建一個測試方法:

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

    現在,讓我們看看將JavaTypeobjectMapper.convertValue()傳遞給objectMapper.convertValue()方法時會發生什麼:

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

    如果我們運行兩個測試,則兩個都將通過。因此,使用objectMapper.convertValue()方法是解決此問題的另一種方法。

    6.創建通用的反序列化方法

    到目前為止,我們已經解決了在將JSON數組反序列化為Java集合時如何解決類轉換問題。在現實世界中,我們可能想創建一個通用方法來處理不同的元素類型。

    現在對於我們來說,這並不是一件艱鉅的工作。我們可以在調用objectMapper.readValue()方法時傳遞一個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方法來構建通用方法,因為它看起來更緊湊?

    現在,讓我們創建一個通用實用程序方法,並將相應的TypeReferenceobjectMapper.readValue()傳遞給objectMapper.readValue()方法:

    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對像傳遞給readValue()方法,並且我們以前已經看到這種方法可以解決類轉換問題。那麼,為什麼在這種情況下我們會看到相同的異常

    這是因為我們的方法是通用的。類型參數T不能在運行時被具體化,即使我們通過TypeReference實例與類型參數T.

    7.用Jackson進行XML反序列化

    除了JSON序列化/反序列化之外,Jackson庫還可用於對XML進行序列化/反序列化。

    讓我們舉一個簡單的例子,檢查將XML反序列化為Java集合時是否可能發生相同的問題。

    首先,讓我們創建一個XML文件books.xml

    <ArrayList>
    
     <item>
    
     <bookId>1</bookId>
    
     <title>A Song of Ice and Fire</title>
    
     <author>George RR 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>
    

    接下來,就像處理JSON文件一樣,我們創建另一個單元測試方法來驗證是否會拋出類轉換異常:

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

    如果我們進行測試,我們的測試將通過。也就是說,在XML反序列化中也會發生相同的問題。

    但是,如果我們知道如何解決JSON反序列化,則將其修復為XML反序列化非常簡單。

    由於XmlMapperObjectMapper,的子類ObjectMapper,因此我們為JSON反序列化解決的所有解決方案也適用於XML反序列化。

    例如,我們可以將TypeReferencexmlMapper.readValue()傳遞給xmlMapper.readValue()方法來解決該問題:

    @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.結論

    在本文中,我們討論了當我們使用傑克遜反序列化JSON或XML時為什麼會出現“ java.util.LinkedHashMap cannot be cast to X ”異常的原因。

    之後,我們通過示例解決了解決問題的不同方法。

    與往常一樣,本文中的代碼都可以在GitHub上找到