Перейти к основному содержимому

Джексон: java.util.LinkedHashMap нельзя преобразовать в X

· 8 мин. чтения

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<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.foreach.jackson.tocollection.Book.*");
}

Мы использовали библиотеку AssertJ , чтобы убедиться, что при вызове bookList.get(0).getBookId() генерируется ожидаемое исключение и что его сообщение совпадает с сообщением, указанным в нашем описании проблемы .

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

2.3. Почему выбрасывается исключение

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

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

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

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

./c427a742e2092db338d91776e6741689.png

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

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

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

Вместо этого мы можем передать объект TypeReference методу 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. Передача JavaType в objectMapper.readValue()

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

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

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

Вернемся к нашему примеру с книгой. В этом примере целевой тип, который мы хотим иметь, — 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()

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

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

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

Теперь давайте посмотрим, что происходит, когда мы передаем объект JavaType в метод 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. В реальном мире мы можем захотеть создать общий метод для обработки различных типов элементов.

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

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

Теперь давайте создадим общий служебный метод и передадим соответствующий объект TypeReference в метод 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.foreach...Book ...

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

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

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

7. Десериализация XML с Джексоном

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

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

Во-первых, давайте создадим XML-файл 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>

Затем, как и в случае с файлом 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.foreach.jackson.tocollection.Book.*");
}

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

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

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

Например, мы можем передать объект TypeReference методу 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. Заключение

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

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

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