CLOVER🍀

That was when it all began.

Jacksonで、JSONをコンテナ型(ListやMapなど)のような型引数を持ったクラスにデシリアライズする

これは、なにをしたくて書いたもの?

Jacksonを使ってJSONをデシリアライズする時に、ObjectMapper#readValueをよく使うわけですが。そういえば、自分で
書いている時にListやMapといったジェネリックな型にデシリアライズしたことがないな、と思い。

工夫が要りそうだなと思い、ちょっと調べてみることに。

デシリアライズ時に型情報を与える

まずは、ObjectMapper#readValue(第2引数がClassクラスの方)のJavadocを見てみます。

readValue(JsonParser p, Class valueType)

よくよく見ると、こんなことが書いてあります。

Note: this method should NOT be used if the result type is a container (Collection or Map. The reason is that due to type erasure, key and value types cannot be introspected when using this method.

結果型をCollectionやMapといったコンテナ型にする場合、このメソッドは使ってはいけません、と。

型情報がなくなるので、キーや値の型がわからなくなるからですね。

このような時は、以下のメソッド(第1引数がStringなどの他のバリエーションのものも含めて)を使うのが良さそうです。

readValue(JsonParser p, TypeReference valueTypeRef)

readValue(JsonParser p, JavaType valueType)

TypeReference

メソッドの説明を見ると、今回の用途にはこちらを使うのがまずは良いのでしょうか?

Method to deserialize JSON content into a Java type, reference to which is passed as argument. Type is passed using so-called "super type token" (see ) and specifically needs to be used if the root type is a parameterized (generic) container type.

readValue(JsonParser p, TypeReference valueTypeRef)

パラメーター化されたコンテナ型をルート型に要求される場合、こちらを使うように、だそうです。

ここで使うものがTypeReferenceクラスで、サブクラスを作成する時に型情報を埋め込みます。

TypeReference

Javadocの例からですが、こんな感じに使います。

TypeReference ref = new TypeReference<List<Integer>>() { };

こちらを、ObjectMapper#readValueの第2引数に渡せばOKです。

もしくは、TypeFactoryを使ってJavaTypeに変換して使います。

which can be passed to methods that accept TypeReference, or resolved using TypeFactory to obtain ResolvedType.

JavaType

もうひとつが、JavaTypeを使う方法ですね。

readValue(JsonParser p, JavaType valueType)

TypeFactoryを使って直接JavaTypeを組み立ててもよいですし、TypeReferenceから変換する方法もあるようです。
※TypeReferenceから変換する場合も、TypeFactoryを使用します。

TypeFactory

こんな感じに使うようです。

ObjectMapper mapper = new ObjectMaper();
JavaType stringCollection = mapper.getTypeFactory().constructCollectionType(List.class, String.class);

では、それぞれ使っていってみましょう。

環境

今回の環境は、こちら。

$ java --version
openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-74-generic", arch: "amd64", family: "unix"

Mavenでの依存関係などは、このように定義。

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.3</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.19.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

動作確認は、テストコードで行います。

テストコードの雛形とお題

テストコードの雛形は、こちら。

src/test/java/org/littlewings/jackson/DeserializeJsonWithTypeTest.java

package org.littlewings.jackson;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class DeserializeJsonWithTypeTest {
    // ここに、テストを書く!
}

お題としては、以下のクラスを題材に、ListやMapに格納したインスタンスをJSONにシリアライズ、デシリアライズする
パターンをいくつか試してみようと思います。

src/test/java/org/littlewings/jackson/Person.java

package org.littlewings.jackson;

public class Person {
    String lastName;
    String firstName;
    int age;

    public static Person create(String lastName, String firstName, int age) {
        Person person = new Person();
        person.setLastName(lastName);
        person.setFirstName(firstName);
        person.setAge(age);

        return person;
    }

    // getter/setterは省略
}

List

まずはListで試してみましょう。

Classを指定する

最初は、ObjectMapper#readValueにClassを指定してみます。

    @Test
    public void nonProvideTypeAsList() throws JsonProcessingException {
        List<Person> persons =
                List.of(
                        Person.create("磯野", "カツオ", 11),
                        Person.create("フグ田", "タラオ", 3)
                );

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("[{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}]");

        List<Object> deserializedPersons = mapper.readValue(json, List.class);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(0)).extracting("lastName").isEqualTo("磯野");
        assertThat(deserializedPersons.get(0)).extracting("firstName").isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0)).extracting("age").isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(1)).extracting("lastName").isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1)).extracting("firstName").isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1)).extracting("age").isEqualTo(3);
    }

このようにObjectMapper#readValueの第2引数にList.classとか渡してしまうと、その中に入るのはこのケースだと
LinkedHashMapのインスタンスになります。

        List<Object> deserializedPersons = mapper.readValue(json, List.class);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(0)).extracting("lastName").isEqualTo("磯野");
        assertThat(deserializedPersons.get(0)).extracting("firstName").isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0)).extracting("age").isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(1)).extracting("lastName").isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1)).extracting("firstName").isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1)).extracting("age").isEqualTo(3);

こんな感じに書いてもコンパイル自体は通りますが、Listに格納されたデータを扱う時にキャストに失敗します。

        List<Person> deserializedPersons = mapper.readValue(json, List.class);

こちらが、その時の例外メッセージ。

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class org.littlewings.jackson.Person (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; org.littlewings.jackson.Person is in unnamed module of loader 'app')

もうちょっと具体的に型を指定したとしても、これくらいでしょうか。

        List<LinkedHashMap<String, Object>> deserializedPersons = mapper.readValue(json, List.class);
TypeReferenceを使う

次は、TypeReferenceを使ってみましょう。

    @Test
    public void provideTypeReferenceAsList() throws JsonProcessingException {
        List<Person> persons =
                List.of(
                        Person.create("磯野", "カツオ", 11),
                        Person.create("フグ田", "タラオ", 3)
                );

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("[{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}]");

        List<Person> deserializedPersons = mapper.readValue(json, new TypeReference<>() {
        });

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0).getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get(0).getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0).getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1).getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1).getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1).getAge()).isEqualTo(3);
    }

今回は、ObjectMapper#readValueの第2引数にいきなりTypeReferenceのサブクラスを作成して渡しています。

        List<Person> deserializedPersons = mapper.readValue(json, new TypeReference<>() {
        });

すると、Listの中に格納されるのがPersonのインスタンスになります(LinkedHashMapではなく)。

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0).getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get(0).getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0).getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1).getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1).getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1).getAge()).isEqualTo(3);
JavaTypeを使う

続いて、JavaType。

    @Test
    public void provideJavaTypeAsList() throws JsonProcessingException {
        List<Person> persons =
                List.of(
                        Person.create("磯野", "カツオ", 11),
                        Person.create("フグ田", "タラオ", 3)
                );

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("[{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}]");

        TypeFactory typeFactory = mapper.getTypeFactory();
        CollectionType collectionType = typeFactory.constructCollectionType(List.class, Person.class);

        List<Person> deserializedPersons = mapper.readValue(json, collectionType);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0).getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get(0).getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0).getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1).getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1).getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1).getAge()).isEqualTo(3);
    }

こんな感じで、ObjectMapperからTypeFactoryを取得して、TypeFactory#construct〜Typeを使ってJavaTypeを
構築します。

        TypeFactory typeFactory = mapper.getTypeFactory();
        CollectionType collectionType = typeFactory.constructCollectionType(List.class, Person.class);

        List<Person> deserializedPersons = mapper.readValue(json, collectionType);

construct〜Typeなメソッドは、配列、コレクション、Map、ParameticTypeなどいろいろあります。

TypeFactory

TypeReferenceをJavaTypeに変換して使う

最後に、TypeReferenceをJavaTypeに変換してみましょう。

    @Test
    public void provideTypeReferenceToJavaTypeAsList() throws JsonProcessingException {
        List<Person> persons =
                List.of(
                        Person.create("磯野", "カツオ", 11),
                        Person.create("フグ田", "タラオ", 3)
                );

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("[{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}]");

        TypeReference<List<Person>> typeReference = new TypeReference<>() {
        };
        JavaType javaType = mapper.getTypeFactory().constructType(typeReference);

        List<Person> deserializedPersons = mapper.readValue(json, javaType);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0).getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get(0).getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0).getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1).getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1).getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1).getAge()).isEqualTo(3);
    }

こんな感じで、TypeReferenceのサブクラスのインスタンスを作成した後に、TypeFactory#constructTypeを使って
JavaTypeを構築することができます。

        TypeReference<List<Person>> typeReference = new TypeReference<>() {
        };
        JavaType javaType = mapper.getTypeFactory().constructType(typeReference);

        List<Person> deserializedPersons = mapper.readValue(json, javaType);

だいたい、使い方はわかった気がしますね。

Mapで使う

もうひとつ、Mapでバリエーションを試してみましょう。

Classを指定する

まずは、ClassをObjectMapper#readValueに指定するパターン。

    @Test
    public void nonProvideTypeAsMap() throws JsonProcessingException {
        Map<String, Person> persons = new LinkedHashMap<>();
        persons.put("katsuo", Person.create("磯野", "カツオ", 11));
        persons.put("tarao", Person.create("フグ田", "タラオ", 3));

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("{\"katsuo\":{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},\"tarao\":{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}}");

        Map<String, Object> deserializedPersons = mapper.readValue(json, Map.class);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get("katsuo")).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get("katsuo")).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get("katsuo")).extracting("lastName").isEqualTo("磯野");
        assertThat(deserializedPersons.get("katsuo")).extracting("firstName").isEqualTo("カツオ");
        assertThat(deserializedPersons.get("katsuo")).extracting("age").isEqualTo(11);

        assertThat(deserializedPersons.get("tarao")).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get("tarao")).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get("tarao")).extracting("lastName").isEqualTo("フグ田");
        assertThat(deserializedPersons.get("tarao")).extracting("firstName").isEqualTo("タラオ");
        assertThat(deserializedPersons.get("tarao")).extracting("age").isEqualTo(3);
    }

こちらは、値がLinkedHashMapなMapとなります。

        List<Object> deserializedPersons = mapper.readValue(json, List.class);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get(0)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(0)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(0)).extracting("lastName").isEqualTo("磯野");
        assertThat(deserializedPersons.get(0)).extracting("firstName").isEqualTo("カツオ");
        assertThat(deserializedPersons.get(0)).extracting("age").isEqualTo(11);

        assertThat(deserializedPersons.get(1)).isNotInstanceOf(Person.class);
        assertThat(deserializedPersons.get(1)).isInstanceOf(LinkedHashMap.class);
        assertThat(deserializedPersons.get(1)).extracting("lastName").isEqualTo("フグ田");
        assertThat(deserializedPersons.get(1)).extracting("firstName").isEqualTo("タラオ");
        assertThat(deserializedPersons.get(1)).extracting("age").isEqualTo(3);
TypeReferenceを使う

TypeReferenceを使った場合は、こんな感じに。

    @Test
    public void provideTypeReferenceAsMap() throws JsonProcessingException {
        Map<String, Person> persons = new LinkedHashMap<>();
        persons.put("katsuo", Person.create("磯野", "カツオ", 11));
        persons.put("tarao", Person.create("フグ田", "タラオ", 3));

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("{\"katsuo\":{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},\"tarao\":{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}}");

        Map<String, Person> deserializedPersons = mapper.readValue(json, new TypeReference<>() {
        });

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get("katsuo")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("katsuo").getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get("katsuo").getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get("katsuo").getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get("tarao")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("tarao").getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get("tarao").getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get("tarao").getAge()).isEqualTo(3);
    }

Listの時と同じように、TypeReferenceでMapに関する型情報を指定してObjectMapper#readValueに与えることで、
Map<String, Person>としてデシリアライズできます。

        Map<String, Person> deserializedPersons = mapper.readValue(json, new TypeReference<>() {
        });

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get("katsuo")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("katsuo").getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get("katsuo").getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get("katsuo").getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get("tarao")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("tarao").getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get("tarao").getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get("tarao").getAge()).isEqualTo(3);
JavaTypeを使う

JavaTypeを使った場合。

    @Test
    public void provideJavaTypeAsMap() throws JsonProcessingException {
        Map<String, Person> persons = new LinkedHashMap<>();
        persons.put("katsuo", Person.create("磯野", "カツオ", 11));
        persons.put("tarao", Person.create("フグ田", "タラオ", 3));

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("{\"katsuo\":{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},\"tarao\":{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}}");

        TypeFactory typeFactory = mapper.getTypeFactory();
        MapType mapType = typeFactory.constructMapType(Map.class, String.class, Person.class);

        Map<String, Person> deserializedPersons = mapper.readValue(json, mapType);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get("katsuo")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("katsuo").getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get("katsuo").getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get("katsuo").getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get("tarao")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("tarao").getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get("tarao").getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get("tarao").getAge()).isEqualTo(3);
    }

Mapを対象とする場合は、TypeFactory#constructMapTypeを使います。

        TypeFactory typeFactory = mapper.getTypeFactory();
        MapType mapType = typeFactory.constructMapType(Map.class, String.class, Person.class);

        Map<String, Person> deserializedPersons = mapper.readValue(json, mapType);
TypeReferenceからJavaTypeに変換して使う

最後は、TypeReferenceからJavaTypeに変換してObjectMapper#readValueに適用します。

    @Test
    public void provideTypeReferenceToJavaTypeAsMap() throws JsonProcessingException {
        Map<String, Person> persons = new LinkedHashMap<>();
        persons.put("katsuo", Person.create("磯野", "カツオ", 11));
        persons.put("tarao", Person.create("フグ田", "タラオ", 3));

        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(persons);

        assertThat(json).isEqualTo("{\"katsuo\":{\"lastName\":\"磯野\",\"firstName\":\"カツオ\",\"age\":11},\"tarao\":{\"lastName\":\"フグ田\",\"firstName\":\"タラオ\",\"age\":3}}");

        TypeReference<Map<String, Person>> typeReference = new TypeReference<>() {
        };
        JavaType javaType = mapper.getTypeFactory().constructType(typeReference);

        Map<String, Person> deserializedPersons = mapper.readValue(json, javaType);

        assertThat(deserializedPersons).hasSize(2);
        assertThat(deserializedPersons.get("katsuo")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("katsuo").getLastName()).isEqualTo("磯野");
        assertThat(deserializedPersons.get("katsuo").getFirstName()).isEqualTo("カツオ");
        assertThat(deserializedPersons.get("katsuo").getAge()).isEqualTo(11);

        assertThat(deserializedPersons.get("tarao")).isInstanceOf(Person.class);
        assertThat(deserializedPersons.get("tarao").getLastName()).isEqualTo("フグ田");
        assertThat(deserializedPersons.get("tarao").getFirstName()).isEqualTo("タラオ");
        assertThat(deserializedPersons.get("tarao").getAge()).isEqualTo(3);
    }

こちらはListの時と同様に、TypeFactory#constructTypeを使ってTypeReferenceを元にJavaTypeを構築すればOKです。

        TypeReference<Map<String, Person>> typeReference = new TypeReference<>() {
        };
        JavaType javaType = mapper.getTypeFactory().constructType(typeReference);

        Map<String, Person> deserializedPersons = mapper.readValue(json, javaType);

まとめ

Jacksonを使って、型引数を持ったクラスにデシリアライズする方法を見てみました。

あまり考えたことがなかったのと、調べようとしてもちょっと見つけにくかった感じがしたので、自分でもまとめつつ
Javadocも眺めてみました。

調べるとTypeReferenceの方が最初に見つかるのですが、Javadocを見ているとJavaTypeのことに気づいたりするので、
見返してみると発見がありますね、と…。