Рубрики
Без рубрики

Джексон: java.util.LinkedHashMap не может быть приведен к X

Узнайте, почему “java.lang.ClassCastException: java.util.LinkedHashMap не может быть приведен к X” возникает исключение и как решить проблему

Автор оригинала: Kai Yuan.

1. Обзор

Jackson-это широко используемая библиотека Java, которая позволяет нам удобно сериализовать/десериализовать JSON или XML.

Иногда мы можем столкнуться ” java.lang.ClassCastException: java.util.LinkedHashMap не может быть приведен к X ” , когда мы пытаемся десериализовать JSON или XML в коллекцию объектов.

В этом уроке мы обсудим, почему может возникнуть упомянутое исключение и как решить эту проблему.

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
}

Теперь предположим, что у нас есть книги.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 :

2.2. Десериализация JSON в список<Книга>

Давайте посмотрим, сможем ли мы воспроизвести проблему приведения классов, десериализуя этот файл JSON в объект List и считывая из него элементы:

@Test
void givenJsonString_whenDeserializingToList_thenThrowingClassCastException() 
  throws JsonProcessingException {
    String jsonString = readFile("/to-java-collection/books.json");
    List 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 , чтобы убедиться, что ожидаемое исключение возникает при вызове book List.get(0).getBookId() и что его сообщение совпадает с сообщением, указанным в нашей постановке задачи .

Тест проходит, что означает, что мы успешно воспроизвели проблему.

2.3. Почему Возникает Исключение

Теперь, если мы более внимательно рассмотрим сообщение об исключении: ” class java.util.LinkedHashMap не может быть приведен к классу … Книга “, может возникнуть пара вопросов.

Мы объявили переменную bookList с типом List , но почему Джексон пытается привести тип LinkedHashMap к нашему классу Book ? Кроме того, откуда берется LinkedHashMap ?

Во-первых, действительно, мы объявили bookList с типом List. Однако, когда мы вызвали метод ObjectMapper.readValue () , мы передали ArrayList.class как объект класса . Поэтому Джексон десериализует содержимое JSON в объект ArrayList , но он понятия не имеет, какого типа элементы должны быть в объекте ArrayList .

Во-вторых, когда Джексон пытается десериализовать объект в JSON, но информация о целевом типе не указана, он будет использовать тип по умолчанию: LinkedHashMap . Другими словами, после десериализации мы получим объект ArrayList . В Map ключами являются имена свойств — например, ” BookID “, ” title ” и так далее. Значения – это значения соответствующих свойств:

Теперь, когда мы поняли причину проблемы, давайте обсудим, как ее решить.

3. Передача ссылки на тип в ObjectMapper.readValue()

Чтобы решить проблему, нам нужно каким-то образом сообщить Джексону тип элемента. Однако компилятор не позволяет нам делать что-то вроде ObjectMapper.readValue(jsonString, ArrayList.class) .

Вместо этого мы можем передать TypeReference объект к ObjectMapper.readValue(содержимое строки, значение TypeReference valueTypeRef) метод. В этом случае нам просто нужно передать new TypeReference>() {} в качестве второго параметра:

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

Если мы проведем тест, он пройдет. Итак, проходя Ссылка на тип объект решает нашу проблему.

4. Передача JavaType в ObjectMapper.readValue()

В предыдущем разделе мы говорили о передаче класса объекта или Введите ссылку object в качестве второго параметра для вызова метода ObjectMapper.readValue () .

Метод ObjectMapper.readValue() по-прежнему принимает объект JavaType в качестве второго параметра. Тип Java является базовым классом классов токенов типа. Он будет использоваться десериализатором, чтобы десериализатор знал, какой тип цели используется во время десериализации.

Мы можем построить Тип Java объект через Экземпляр TypeFactory , и мы можем получить объект Type Factory из ObjectMapper.getTypeFactory() .

Давайте вернемся к нашему книжному примеру. В этом примере целевой тип, который мы хотим иметь, – ArrayList . Поэтому мы можем построить Тип коллекции с этим требованием:

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

Теперь давайте напишем модульный тест, чтобы увидеть, решит ли ваша проблема передача типа Java в метод 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 bookList = objectMapper.readValue(jsonString, listType);
    assertThat(bookList.get(0)).isInstanceOf(Book.class);
    assertThat(bookList).isEqualTo(expectedBookList);
}

Тест пройдет, если мы его запустим. Следовательно, проблема может быть решена и таким образом.

5. Использование объекта JsonNode и метода ObjectMapper.convertValue()

Мы видели решение передачи a TypeReference или JavaType object к методу ObjectMapper.readValue () .

В качестве альтернативы мы можем работать с узлами древовидной модели в Jackson , а затем преобразовать объект JsonNode в нужный тип, вызвав метод ObjectMapper.convertValue () .

Аналогично, мы можем передать объект Type Reference или Java Type в метод ObjectMapper.convertValue () .

Давайте рассмотрим каждый подход в действии.

Во-первых, давайте создадим метод тестирования с использованием Ссылка на тип объект и метод ObjectMapper.convertValue() :

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

Теперь давайте посмотрим, что происходит, когда мы передаем тип Java объект методу ObjectMapper.convertValue() :

@Test
void givenJsonString_whenDeserializingWithConvertValueAndJavaType_thenGetExpectedList() 
  throws JsonProcessingException {
    String jsonString = readFile("/to-java-collection/books.json");
    JsonNode jsonNode = objectMapper.readTree(jsonString);
    List 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. В реальном мире мы можем захотеть создать универсальный метод для обработки различных типов элементов.

Теперь это не будет для нас трудной работой. Мы можем передать объект JavaType при вызове метода ObjectMapper.readValue () |:

public static  List jsonArrayToList(String json, Class 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 bookList = JsonToCollectionUtil.jsonArrayToList(jsonString, Book.class);
    assertThat(bookList.get(0)).isInstanceOf(Book.class);
    assertThat(bookList).isEqualTo(expectedBookList);
}

Тест пройдет, если мы его запустим.

Почему бы не использовать Ссылку на тип подход для создания универсального метода, поскольку он выглядит более компактным?

Теперь давайте создадим универсальный служебный метод и передадим соответствующую ссылку На тип объекта в метод ObjectMapper.readValue() :

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

Метод выглядит простым. Если мы еще раз запустим метод тестирования, мы получим:

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

Ой, произошло исключение!

Мы прошли TypeReference object к методу readValue () , и мы ранее видели, что этот способ решит проблему приведения класса. Итак, почему мы видим одно и то же исключение в этом случае?

Это потому, что наш метод является универсальным. Параметр типа T не может быть овеществлен во время выполнения , даже если мы передадим Ссылка на тип экземпляр с параметром type T.

7. Десериализация XML С Помощью Джексона

Помимо сериализации/десериализации JSON, библиотека Джексона также может использоваться для сериализации/десериализации XML.

Давайте приведем краткий пример, чтобы проверить, может ли возникнуть та же проблема при десериализации XML в коллекции Java.

Во-первых, давайте создадим XML-файл books.xml :


    
        1
        A Song of Ice and Fire
        George R. R. Martin
    
    
        2
        The Hitchhiker's Guide to the Galaxy
        Douglas Adams
    
    
        3
        Hackers And Painters
        Paul Graham
    

Затем, как и в случае с файлом JSON, мы создаем другой метод модульного тестирования, чтобы проверить, будет ли вызвано исключение приведения класса:

@Test
void givenXml_whenDeserializingToList_thenThrowingClassCastException() 
  throws JsonProcessingException {
    String xml = readFile("/to-java-collection/books.xml");
    List 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.

С XmlMapper является подклассом ObjectMapper, все решения, которые мы рассмотрели для десериализации JSON, также работают для десериализации XML.

Например, мы можем передать Введите ссылку объект на xmlMapper.readValue() метод для решения проблемы:

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

8. Заключение

В этой статье мы обсудили, почему мы можем получить ” java.util.LinkedHashMap не может быть приведен к исключению X “, когда мы используем Jackson для десериализации JSON или XML.

После этого мы рассмотрели различные способы решения проблемы на примерах.

Как всегда, код в этой записи полностью доступен на GitHub .