1. 概要

Jackson は広く使用されているJavaライブラリであり、JSONまたはXMLを便利にシリアル化/逆シリアル化できます。

JSONまたはXMLをオブジェクトのコレクションに逆シリアル化しようとすると、「 java.lang.ClassCastException:java.util.LinkedHashMapを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
}

ここで、3冊の本を含むJSON配列で構成されるbooks.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の例を次のように逆シリアル化しようとするとどうなるかを確認しますリスト

2.2. JSONをデシリアライズしてリスト

このJSONファイルを逆シリアル化することでクラスキャストの問題を再現できるかどうかを見てみましょう。 リストオブジェクトとそこからの要素の読み取り:

@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. 例外がスローされる理由

ここで、例外メッセージ「 classjava.util.LinkedHashMapをクラスにキャストできません…Book」を詳しく見ると、いくつかの質問が出てくる可能性があります。

変数を宣言しましたブックリストタイプでリスト 、しかしなぜジャクソンはキャストしようとするのですか LinkedHashMap 私たちに入力してくださいクラス? さらに、 LinkedHashMap はどこから来たのですか?

まず、確かに私たちは宣言しましたブックリストタイプでリストしかし、私たちが電話したとき objectMapper.readValue() 方法、 ArrayList.classをClassオブジェクトとして渡しました。 したがって、JacksonはJSONコンテンツをArrayListオブジェクトに逆シリアル化しますが、ArrayListオブジェクトにどのタイプの要素を含めるべきかはわかりません。

次に、 JacksonがJSONでオブジェクトを逆シリアル化しようとしたが、ターゲットタイプ情報が指定されていない場合、デフォルトのタイプであるLinkedHashMapが使用されます。 つまり、逆シリアル化後、次のようになります。 配列リスト物体。 Map では、キーはプロパティの名前です。たとえば、「 bookId 」、「title」などです。 値は、対応するプロパティの値です。

問題の原因がわかったので、それを解決する方法について説明しましょう。

3. TypeReferenceobjectMapper.readValue()に渡す

この問題を解決するには、どういうわけかジャクソンに要素のタイプを知らせる必要があります。 ただし、コンパイラでは、次のようなことはできません。 objectMapper.readValue(jsonString、ArrayList 。クラス)

その代わり、 TypeReferenceオブジェクトをobjectMapper.readValue(String content、TypeReference valueTypeRef)メソッドに渡すことができます。 この場合、合格する必要があります新しいTypeReference >(){} 2番目のパラメーターとして:

@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オブジェクトを2番目のパラメーターとして渡してobjectMapper.readValue()メソッドを呼び出す方法について説明しました。

objectMapper.readValue()メソッドは、2番目のパラメーターとしてJavaTypeオブジェクトを引き続き受け入れます。 JavaTypeは、タイプトークンクラスの基本クラスです。 これはデシリアライザーによって使用されるため、デシリアライザーはデシリアライズ中にターゲットタイプが何であるかを認識します。 

TypeFactoryインスタンスを介してJavaTypeオブジェクトを構築でき、 objectMapper.getTypeFactory()からTypeFactoryオブジェクトを取得できます。

私たちの本の例に戻りましょう。 この例では、必要なターゲットタイプは次のとおりです。 配列リスト 。 したがって、次の要件でCollectionTypeを作成できます。

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

次に、 JavaTypereadValue()メソッドに渡すことで問題が解決するかどうかを確認するために、単体テストを作成しましょう。

@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()メソッドに渡すソリューションを見てきました。

または、 Jackson のツリーモデルノードを操作してから、objectMapper.convertValue()メソッドを呼び出してJsonNodeオブジェクトを目的のタイプに変換できます。

同様に、 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);
}

2つのテストを実行すると、両方とも合格します。 したがって、 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アプローチを使用して、よりコンパクトに見える一般的なメソッドを構築してみませんか?

次に、ジェネリックユーティリティメソッドを作成し、対応する 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.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 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.baeldung.jackson.tocollection.Book.*");
}

テストを実行すると、テストに合格します。 つまり、XMLの逆シリアル化でも同じ問題が発生します。

ただし、JSONの逆シリアル化を解決する方法を知っていれば、XMLの逆シリアル化で修正するのは非常に簡単です。

XmlMapperObjectMapperのサブクラスであるため、 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. 結論

この記事では、Jacksonを使用してJSONまたはXMLを逆シリアル化すると、「java.util.LinkedHashMapをXにキャストできません」という例外が発生する理由について説明しました。

その後、例を通して問題を解決するためのさまざまな方法に取り組みました。

いつものように、この記事のコードはすべてGitHub利用できます。