«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
}

Теперь предположим, что у нас есть файл 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\u003cBook\u003e:

2.2. Десериализация JSON в List\u003cBook\u003e

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

@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. Почему возникает исключение

Теперь, если мы внимательно посмотрим на сообщение об исключении: «класс java.util.LinkedHashMap не может быть приведен к классу … Book», может возникнуть пара вопросов.

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

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

Во-вторых, когда Джексон пытается десериализовать объект в JSON, но информация о целевом типе не указана, он будет использовать тип по умолчанию: LinkedHashMap. Другими словами, после десериализации мы получим объект ArrayList\u003cLinkedHashMap\u003e. В Карте ключами являются имена свойств, например, «bookId», «title» и т. д. Значения являются значениями соответствующих свойств:

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

3. Передача TypeReference в objectMapper.readValue()

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

Вместо этого мы можем передать объект TypeReference методу objectMapper.readValue(String content, TypeReference valueTypeRef). В этом случае нам просто нужно передать new TypeReference\u003cList\u003cBook\u003e\u003e() {} в качестве второго параметра:

@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 в objectMapper.readValue()

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

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

Мы можем создать объект JavaType через экземпляр TypeFactory, и мы можем получить объект TypeFactory из objectMapper.getTypeFactory().

Вернемся к нашему примеру с книгой. В этом примере целевой тип, который нам нужен, — ArrayList\u003cBook\u003e. Следовательно, мы можем создать CollectionType с этим требованием:

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

«

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

«Теперь давайте напишем модульный тест, чтобы увидеть, решит ли наша проблема передача типа JavaType в метод readValue():

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

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

Мы видели решение передачи объекта TypeReference или JavaType в метод objectMapper.readValue().

Кроме того, мы можем работать с узлами модели дерева в Джексоне, а затем преобразовать объект JsonNode в нужный тип, вызвав метод objectMapper.convertValue().

Точно так же мы можем передать объект TypeReference или JavaType в метод 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);
}

Во-первых, давайте создадим тестовый метод, используя объект TypeReference и метод 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);
}

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

Если мы запустим два теста, оба они пройдут. Поэтому использование метода objectMapper.convertValue() является альтернативным способом решения проблемы.

6. Создание универсального метода десериализации

До сих пор мы рассматривали, как решить проблему приведения классов при десериализации массива JSON в коллекции Java. В реальном мире мы можем захотеть создать общий метод для обработки различных типов элементов.

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

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

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

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

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

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

К сожалению, произошло исключение!

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

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

7. Десериализация XML с помощью Jackson

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

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

<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>

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

@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, мы создадим еще один метод модульного тестирования, чтобы проверить, будет ли выдано исключение приведения класса :

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

Однако, если мы знаем, как решить десериализацию JSON, это довольно просто исправить в десериализации XML.

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

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

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

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

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

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