Jackson 工具类详解:ObjectMapper 配置、泛型擦除、TypeReference 与 JavaType

Jackson 工具类详解:ObjectMapper 配置、泛型擦除、TypeReference 与 JavaType

一、前言

在 Java 项目中,JSON 转换是非常常见的功能。前后端接口交互、配置解析、对象缓存、日志记录等场景中,都会用到对象和 JSON 字符串之间的转换。

常见的 JSON 工具有 Gson、FastJson、Jackson。实际企业项目中,Jackson 使用得非常多,尤其是在 Spring Boot 中,默认的 JSON 处理框架就是 Jackson。

本文主要围绕一个 JsonUtil 工具类展开,重点说明以下几个问题:

复制代码
1. ObjectMapper 常见配置是什么意思
2. Java 时间类型为什么需要额外配置
3. 什么是魔法值,为什么要提取常量
4. Gson、FastJson、Jackson 有什么区别
5. 什么是泛型擦除
6. 为什么 JSON 转 List 时容易变成 LinkedHashMap
7. TypeReference 和 JavaType 分别解决什么问题
8. 如何动态处理 List、Map、List<Map<String, T>> 这种泛型结构

二、JsonUtil 工具类整体结构

项目中的 JSON 工具类一般会封装一个全局的 ObjectMapper,然后提供常用方法:

复制代码
public class JsonUtil {

    private static ObjectMapper OBJECT_MAPPER;

    static {
        OBJECT_MAPPER = JsonMapper.builder()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
                .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
                .configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false)
                .configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false)
                .configure(MapperFeature.USE_ANNOTATIONS, false)
                .addModule(new JavaTimeModule())
                .addModule(new SimpleModule()
                        .addSerializer(LocalDateTime.class,
                                new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                        .addDeserializer(LocalDateTime.class,
                                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                )
                .defaultDateFormat(new SimpleDateFormat(CommonConstants.STANDARD_FORMAT))
                .serializationInclusion(JsonInclude.Include.NON_NULL)
                .build();
    }

    private JsonUtil() {
    }
}

这里的核心就是 ObjectMapper

ObjectMapper 是 Jackson 中最重要的对象,主要负责:

复制代码
1. Java 对象转 JSON 字符串
2. JSON 字符串转 Java 对象
3. 控制序列化和反序列化规则
4. 控制日期格式
5. 处理泛型类型
6. 注册自定义序列化器和反序列化器

三、Jackson 核心配置详解

1. 忽略未知字段

复制代码
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

这个配置用于反序列化。

所谓反序列化,就是把 JSON 字符串转换成 Java 对象。

例如 JSON 中有一个字段:

复制代码
{
  "id": 1,
  "name": "北京",
  "extraField": "多余字段"
}

但是 Java 类中只有:

复制代码
public class TestRegion {
    private Long id;
    private String name;
}

也就是说,Java 类中没有 extraField 这个属性。

默认情况下,Jackson 可能会因为这个未知字段报错。设置为 false 后,Jackson 会忽略 JSON 中多出来的字段。

这样做的好处是提高兼容性。比如前端多传了一个字段,或者第三方接口增加了新字段,后端不会因为这个字段不存在而直接转换失败。

2. 日期不转时间戳

复制代码
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)

这个配置用于序列化。

所谓序列化,就是把 Java 对象转换成 JSON 字符串。

如果 Java 对象中有日期字段,Jackson 默认可能会把日期转成时间戳。

例如:

复制代码
{
  "createTime": 1718000000000
}

这种格式虽然程序能处理,但是可读性比较差。

设置为 false 后,可以让日期按照指定格式输出,例如:

复制代码
{
  "createTime": "2026-06-11 14:30:00"
}

这种格式更适合前后端接口展示和日志排查。

3. 空对象不报错

复制代码
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)

这个配置用于序列化。

如果一个 Java 对象没有任何可序列化的属性,Jackson 默认可能会报错。

设置为 false 后,即使对象没有实际字段,也不会直接报错,通常会序列化成一个空对象:

复制代码
{}

这个配置可以避免某些空对象导致整个 JSON 转换失败。

4. 无效子类型不直接失败

复制代码
.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false)

这个配置用于反序列化。

当 JSON 中携带的类型信息和 Java 中预期的子类型不匹配时,默认可能会报错。

设置为 false 后,Jackson 会尽量继续处理,而不是直接抛出异常。

这个配置主要用于存在多态类型、子类类型标识、复杂继承结构的场景。

5. 日期作为 Map 的 key 时不转时间戳

复制代码
.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false)

这个配置主要影响 Map 的 key。

例如有这种结构:

复制代码
Map<Date, String> map;

如果 Date 类型作为 Map 的 key,默认可能会被转成时间戳格式。

设置为 false 后,日期 key 会按照日期格式进行序列化,而不是直接转成时间戳。

6. 不使用 Jackson 注解

复制代码
.configure(MapperFeature.USE_ANNOTATIONS, false)

Jackson 支持很多注解,例如:

复制代码
@JsonFormat
@JsonIgnore
@JsonProperty
@JsonInclude

这些注解可以控制字段名称、日期格式、是否忽略字段等。

设置为 false 后,Jackson 会忽略这些注解,更多依赖全局配置来处理序列化和反序列化规则。

这个配置适合希望统一管理 JSON 转换规则的场景。

不过要注意,如果项目中大量使用了 Jackson 注解,关闭这个配置可能会导致原本的注解失效。因此这个配置要结合项目实际情况使用。

7. 支持 Java 8 时间类型

复制代码
.addModule(new JavaTimeModule())

Java 8 以后常用的时间类型有:

复制代码
LocalDate
LocalDateTime
LocalTime

这些类型不属于传统的 java.util.Date

Jackson 默认对 Java 8 时间类型的支持不够完整,所以需要添加:

复制代码
JavaTimeModule

这个模块由 jackson-datatype-jsr310 提供,用于支持 Java 8 日期时间 API。

8. 自定义 LocalDateTime 格式

复制代码
.addModule(new SimpleModule()
        .addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
        .addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
)

这段配置是专门处理 LocalDateTime 的。

序列化时:

复制代码
LocalDateTime -> JSON 字符串

反序列化时:

复制代码
JSON 字符串 -> LocalDateTime

例如 Java 对象中有:

复制代码
private LocalDateTime createTime;

序列化后可以变成:

复制代码
{
  "createTime": "2026-06-11 14:30:00"
}

反序列化时,也可以把这个字符串重新转成 LocalDateTime

9. 统一传统 Date 格式

复制代码
.defaultDateFormat(new SimpleDateFormat(CommonConstants.STANDARD_FORMAT))

这个配置主要用于传统日期类型,比如:

复制代码
java.util.Date
java.util.Calendar

如果项目中既有 Date,又有 LocalDateTime,那么通常需要同时配置:

复制代码
1. defaultDateFormat 处理 Date
2. JavaTimeModule / LocalDateTimeSerializer 处理 LocalDateTime

10. null 字段不序列化

复制代码
.serializationInclusion(JsonInclude.Include.NON_NULL)

这个配置表示:只序列化非 null 的字段。

例如 Java 对象:

复制代码
public class User {
    private Long id;
    private String name;
    private String address;
}

如果 address 是 null,序列化后不会出现在 JSON 中。

输出结果可能是:

复制代码
{
  "id": 1,
  "name": "张三"
}

而不是:

复制代码
{
  "id": 1,
  "name": "张三",
  "address": null
}

这样可以减少 JSON 体积,也可以避免前端收到大量 null 字段。

四、魔法值问题

在代码中直接写死的固定值,通常称为魔法值。

例如:

复制代码
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

这里的:

复制代码
yyyy-MM-dd HH:mm:ss

就是一个魔法值。

魔法值的问题主要有两个。

第一,代码可读性差。别人看到这个字符串时,需要自己判断它代表什么含义。

第二,后期维护困难。如果多个地方都写了同一个格式,将来要修改日期格式,就需要到很多地方逐个修改。

更好的做法是提取成常量:

复制代码
public class CommonConstants {
    public static final String STANDARD_FORMAT = "yyyy-MM-dd HH:mm:ss";
}

然后使用:

复制代码
DateTimeFormatter.ofPattern(CommonConstants.STANDARD_FORMAT)

这样代码含义更清楚,也方便统一修改。

在工具类中,已经使用了:

复制代码
.defaultDateFormat(new SimpleDateFormat(CommonConstants.STANDARD_FORMAT))

但是 LocalDateTimeSerializerLocalDateTimeDeserializer 中仍然直接写了:

复制代码
"yyyy-MM-dd HH:mm:ss"

这部分也可以统一改成常量:

复制代码
private static final DateTimeFormatter STANDARD_DATE_TIME_FORMATTER =
        DateTimeFormatter.ofPattern(CommonConstants.STANDARD_FORMAT);

然后:

复制代码
.addSerializer(LocalDateTime.class,
        new LocalDateTimeSerializer(STANDARD_DATE_TIME_FORMATTER))
.addDeserializer(LocalDateTime.class,
        new LocalDateTimeDeserializer(STANDARD_DATE_TIME_FORMATTER))

这样整个工具类的日期格式就统一了。

五、常见 JSON 工具对比

1. Gson

Gson 是 Google 开发的 JSON 处理库。

它的特点是简单、轻量、容易上手。

适合简单的对象和 JSON 互转场景。

但是在复杂日期处理、复杂泛型、企业级定制方面,Gson 的功能相对没有 Jackson 丰富。

2. FastJson

FastJson 是阿里巴巴开源的 JSON 处理库。

它早期以性能较高、使用简单著称。

但是 FastJson 以前出现过一些安全漏洞,所以在实际项目中使用时,需要特别关注版本安全问题,并尽量使用安全版本和安全模式。

3. Jackson

Jackson 是 FasterXML 开发的 JSON 处理库。

它功能强大,生态成熟,支持 JSON、XML、YAML 等多种数据格式。

Spring Boot 默认也使用 Jackson 作为 JSON 处理框架。

Jackson 的优点是:

复制代码
1. 功能丰富
2. 性能较好
3. 支持复杂泛型
4. 支持丰富的配置
5. 和 Spring Boot 集成度高

缺点是配置项较多,初学时学习成本比 Gson 略高。

综合来看,如果是企业级 Spring Boot 项目,Jackson 是非常常见的选择。

六、对象转 JSON 字符串

工具类中对象转字符串方法如下:

复制代码
public static <T> String obj2String(T obj) {
    if (obj == null) {
        return null;
    }
    try {
        return obj instanceof String ? (String) obj : OBJECT_MAPPER.writeValueAsString(obj);
    } catch (JsonProcessingException e) {
        log.warn("Parse Object to String error : {}", e.getMessage());
        return null;
    }
}

这个方法的作用是:

复制代码
Java 对象 -> JSON 字符串

例如:

复制代码
TestRegion region = new TestRegion();
region.setId(1L);
region.setName("北京");
region.setFullName("北京市");
region.setCode("110000");

String json = JsonUtil.obj2String(region);

输出可能是:

复制代码
{"id":1,"name":"北京","fullName":"北京市","code":"110000"}

这里有一个特殊判断:

复制代码
obj instanceof String ? (String) obj : OBJECT_MAPPER.writeValueAsString(obj)

如果传进来的对象本身就是字符串,就直接返回,不再重复转 JSON。

七、格式化输出 JSON

工具类中还有一个格式化输出方法:

复制代码
public static <T> String obj2StringPretty(T obj) {
    if (obj == null) {
        return null;
    }
    try {
        return obj instanceof String ? (String) obj :
                OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
    } catch (JsonProcessingException e) {
        log.warn("Parse Object to String error : {}", e.getMessage());
        return null;
    }
}

这里和普通 obj2String 的区别是:

复制代码
writerWithDefaultPrettyPrinter()

它会让 JSON 字符串变得更美观。

普通 JSON:

复制代码
{"id":1,"name":"北京","fullName":"北京市","code":"110000"}

格式化后:

复制代码
{
  "id" : 1,
  "name" : "北京",
  "fullName" : "北京市",
  "code" : "110000"
}

这种格式更适合调试、日志查看和接口测试。

八、普通对象反序列化

普通对象反序列化方法如下:

复制代码
public static <T> T string2Obj(String str, Class<T> clazz) {
    if (StringUtils.isEmpty(str) || clazz == null) {
        return null;
    }
    try {
        return clazz.equals(String.class) ? (T) str : OBJECT_MAPPER.readValue(str, clazz);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to Object error : {}", e.getMessage());
        return null;
    }
}

这个方法适合 JSON 对应一个普通 Java 对象的情况。

例如 JSON:

复制代码
{
  "id": 1,
  "name": "北京",
  "fullName": "北京市",
  "code": "110000"
}

Java 调用:

复制代码
TestRegion region = JsonUtil.string2Obj(jsonStr, TestRegion.class);

这时 Jackson 能明确知道目标类型是:

复制代码
TestRegion

所以可以正常转换。

九、什么是泛型擦除

泛型擦除是 Java 泛型中的一个重要概念。

简单来说,Java 编译之后,很多泛型信息在运行时会被擦除。

例如代码中写的是:

复制代码
List<TestRegion> list;

但是运行时,JVM 很多时候只知道它是:

复制代码
List

至于 List 里面具体装的是 TestRegion,这个信息在运行时不一定完整保留。

所以如果我们这样写:

复制代码
List list = JsonUtil.string2Obj(jsonStr, List.class);

Jackson 只知道目标类型是 List,但是不知道 List 里面的元素类型。

这时如果 JSON 数组里面是对象,Jackson 默认会把里面的对象转换成:

复制代码
LinkedHashMap

而不是转换成我们想要的:

复制代码
TestRegion

十、Jackson 默认转换规则

当 Jackson 不知道具体目标类型时,会根据 JSON 结构选择默认 Java 类型。

常见规则是:

复制代码
JSON 的 {}  -> LinkedHashMap
JSON 的 []  -> ArrayList

例如 JSON 是:

复制代码
[
  {
    "id": 1,
    "name": "北京",
    "fullName": "北京市",
    "code": "110000"
  }
]

如果直接写:

复制代码
List list = OBJECT_MAPPER.readValue(jsonStr, List.class);

最终得到的不是:

复制代码
List<TestRegion>

而是:

复制代码
ArrayList<LinkedHashMap>

也就是说:

复制代码
外层 [] 变成 ArrayList
里面 {} 变成 LinkedHashMap

这就是很多人遇到的:

复制代码
LinkedHashMap cannot be cast to TestRegion

的根本原因。

不是 Jackson 出错了,而是我们没有告诉 Jackson 泛型里面具体是什么类型。

十一、TypeReference 解决固定复杂泛型

如果泛型结构是固定的,可以使用 TypeReference

工具类方法:

复制代码
public static <T> T string2Obj(String str, TypeReference<T> valueTypeRef) {
    if (StringUtils.isEmpty(str) || valueTypeRef == null) {
        return null;
    }
    try {
        return OBJECT_MAPPER.readValue(str, valueTypeRef);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to Object error : {}", e.getMessage());
        return null;
    }
}

例如 JSON 是:

复制代码
[
  {
    "id": 1,
    "name": "北京",
    "fullName": "北京市",
    "code": "110000"
  }
]

想转成:

复制代码
List<TestRegion>

可以写:

复制代码
List<TestRegion> regionList = JsonUtil.string2Obj(
        jsonStr,
        new TypeReference<List<TestRegion>>() {}
);

这里的:

复制代码
new TypeReference<List<TestRegion>>() {}

把完整泛型信息告诉了 Jackson。

Jackson 就知道:

复制代码
外层是 List
里面的元素是 TestRegion

所以不会再把元素转成 LinkedHashMap

十二、TypeReference 后面的 {} 是什么

很多人第一次看到这个写法会有疑问:

复制代码
new TypeReference<List<TestRegion>>() {}

为什么最后还有一个 {}

这个 {} 表示创建了一个匿名内部类。

Jackson 正是通过这个匿名内部类,读取到父类上携带的泛型信息。

所以这个 {} 不是多余的,它是 TypeReference 这种写法的关键。

如果没有这个匿名内部类,复杂泛型信息就很难被完整保留下来。

十三、TypeReference 处理多层嵌套泛型

如果 JSON 是这种结构:

复制代码
[
  {
    "110000": {
      "id": 1,
      "name": "北京",
      "fullName": "北京市",
      "code": "110000"
    }
  }
]

外层是数组 [],里面是对象 {},对象中的 value 又是一个具体的 Java 对象。

对应 Java 类型是:

复制代码
List<Map<String, TestRegion>>

含义是:

复制代码
外层 []       -> List
里面 {}       -> Map
Map 的 key    -> String
Map 的 value  -> TestRegion

写法如下:

复制代码
List<Map<String, TestRegion>> mapList = JsonUtil.string2Obj(
        jsonStr,
        new TypeReference<List<Map<String, TestRegion>>>() {}
);

这个适合类型固定的场景。

如果代码里已经明确知道就是:

复制代码
List<Map<String, TestRegion>>

那么用 TypeReference 最简单。

十四、JavaType 解决动态泛型

TypeReference 适合固定泛型。

但是如果类型是调用方法时传进来的,就更适合使用 JavaType

例如工具类中有方法:

复制代码
public static <T> List<T> string2List(String str, Class<T> clazz)

这里的 T 不是写死的。

调用时可能是:

复制代码
JsonUtil.string2List(jsonStr, TestRegion.class);
JsonUtil.string2List(jsonStr, User.class);
JsonUtil.string2List(jsonStr, Order.class);

这种情况下,泛型类型是在运行时才确定的。

所以需要用 Jackson 的 TypeFactory 动态构造 JavaType

十五、动态解决 List 泛型擦除

工具类中的 List 转换方法:

复制代码
public static <T> List<T> string2List(String str, Class<T> clazz) {
    if (StringUtils.isEmpty(str) || clazz == null) {
        return null;
    }

    JavaType javaType = OBJECT_MAPPER
            .getTypeFactory()
            .constructParametricType(List.class, clazz);

    try {
        return OBJECT_MAPPER.readValue(str, javaType);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to Object error : {}", e.getMessage());
        return null;
    }
}

核心代码是:

复制代码
constructParametricType(List.class, clazz)

它的意思是动态构造:

复制代码
List<T>

例如调用:

复制代码
List<TestRegion> list = JsonUtil.string2List(jsonStr, TestRegion.class);

这时:

复制代码
clazz = TestRegion.class

所以最终构造出来的类型就是:

复制代码
List<TestRegion>

这样 Jackson 就知道 List 里面每一个元素都是 TestRegion,不会再默认转成 LinkedHashMap

十六、如果想明确使用 ArrayList

在实际开发中,一般建议返回接口类型:

复制代码
List<T>

而不是具体实现类:

复制代码
ArrayList<T>

因为接口更灵活。

但是如果确实想明确构造 ArrayList<T>,可以写:

复制代码
public static <T> ArrayList<T> string2ArrayList(String str, Class<T> clazz) {
    if (StringUtils.isEmpty(str) || clazz == null) {
        return null;
    }

    JavaType javaType = OBJECT_MAPPER
            .getTypeFactory()
            .constructCollectionType(ArrayList.class, clazz);

    try {
        return OBJECT_MAPPER.readValue(str, javaType);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to ArrayList error : {}", e.getMessage());
        return null;
    }
}

核心代码是:

复制代码
constructCollectionType(ArrayList.class, clazz)

它表示构造:

复制代码
ArrayList<T>

例如:

复制代码
ArrayList<TestRegion> list = JsonUtil.string2ArrayList(jsonStr, TestRegion.class);

十七、动态解决 Map 泛型擦除

工具类中的 Map 转换方法:

复制代码
public static <T> Map<String, T> string2Map(String str, Class<T> valueClass) {
    if (StringUtils.isEmpty(str) || valueClass == null) {
        return null;
    }

    JavaType javaType = OBJECT_MAPPER
            .getTypeFactory()
            .constructMapType(LinkedHashMap.class, String.class, valueClass);

    try {
        return OBJECT_MAPPER.readValue(str, javaType);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to Object error : {}", e.getMessage());
        return null;
    }
}

核心代码是:

复制代码
constructMapType(LinkedHashMap.class, String.class, valueClass)

它的意思是构造:

复制代码
LinkedHashMap<String, T>

三个参数分别表示:

复制代码
LinkedHashMap.class  -> Map 的具体实现类
String.class         -> key 的类型
valueClass           -> value 的类型

例如 JSON:

复制代码
{
  "110000": {
    "id": 1,
    "name": "北京",
    "fullName": "北京市",
    "code": "110000"
  }
}

调用:

复制代码
Map<String, TestRegion> map = JsonUtil.string2Map(jsonStr, TestRegion.class);

最终构造出来的类型就是:

复制代码
LinkedHashMap<String, TestRegion>

这里用 LinkedHashMap 是因为 JSON 对象 {} 本质上就是键值对结构。并且 LinkedHashMap 可以保持字段的插入顺序。


十八、为什么 Map 方法里写 LinkedHashMap,不写 ArrayList

因为这个方法本身就是处理 Map 结构的:

复制代码
public static <T> Map<String, T> string2Map(String str, Class<T> valueClass)

它适合处理最外层是 {} 的 JSON。

例如:

复制代码
{
  "110000": {
    "id": 1,
    "name": "北京"
  }
}

这种结构应该用 Map 接收。

但是如果 JSON 最外层是数组:

复制代码
[
  {
    "id": 1,
    "name": "北京"
  }
]

那就不能用 string2Map,而应该用 string2List

可以这样记:

复制代码
JSON 最外层是 {}  -> 用 Map / LinkedHashMap
JSON 最外层是 []  -> 用 List / ArrayList

所以:

复制代码
constructMapType(LinkedHashMap.class, String.class, valueClass)

只是在构造 Map 类型。

如果要构造数组或集合类型,应该用:或者:

复制代码
constructParametricType(...)

十九、动态解决 List<Map<String, T>> 泛型擦除

如果 JSON 是这种嵌套结构:

复制代码
[
  {
    "110000": {
      "id": 1,
      "name": "北京",
      "fullName": "北京市",
      "code": "110000"
    }
  },
  {
    "310000": {
      "id": 2,
      "name": "上海",
      "fullName": "上海市",
      "code": "310000"
    }
  }
]

它的结构是:

复制代码
外层 []       -> List
里面 {}       -> Map
Map 的 key    -> String
Map 的 value  -> T

Java 类型就是:

复制代码
List<Map<String, T>>

动态写法如下:

复制代码
public static <T> List<Map<String, T>> string2ListMap(String str, Class<T> valueClass) {
    if (StringUtils.isEmpty(str) || valueClass == null) {
        return null;
    }

    JavaType mapType = OBJECT_MAPPER
            .getTypeFactory()
            .constructMapType(LinkedHashMap.class, String.class, valueClass);

    JavaType listType = OBJECT_MAPPER
            .getTypeFactory()
            .constructCollectionType(List.class, mapType);

    try {
        return OBJECT_MAPPER.readValue(str, listType);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to List<Map<String, T>> error : {}", e.getMessage());
        return null;
    }
}

调用方式:

复制代码
List<Map<String, TestRegion>> list =
        JsonUtil.string2ListMap(jsonStr, TestRegion.class);

这个方法的构造过程分成两步。

第一步,先构造里面的 Map 类型:

复制代码
Map<String, TestRegion>

对应代码:

复制代码
JavaType mapType = OBJECT_MAPPER
        .getTypeFactory()
        .constructMapType(LinkedHashMap.class, String.class, TestRegion.class);

第二步,再构造外层的 List 类型:

复制代码
List<Map<String, TestRegion>>

对应代码:这样 Jackson 就知道完整结构了。


二十、多层嵌套泛型如何处理

有些接口返回的数据结构可能更加复杂,例如:

复制代码
TianYanChaResponse<TianYanChaListResult<TianYanChaResultItem190>>

这种类型属于多层嵌套泛型。

如果类型是固定的,可以直接用 TypeReference

复制代码
TianYanChaResponse<TianYanChaListResult<TianYanChaResultItem190>> response =
        OBJECT_MAPPER.readValue(
                jsonStr,
                new TypeReference<TianYanChaResponse<TianYanChaListResult<TianYanChaResultItem190>>>() {}
        );

如果类型需要动态传入,就可以用 JavaType 一层一层构造。

示例:

复制代码
JavaType itemType = OBJECT_MAPPER
        .getTypeFactory()
        .constructType(TianYanChaResultItem190.class);

JavaType listResultType = OBJECT_MAPPER
        .getTypeFactory()
        .constructParametricType(TianYanChaListResult.class, itemType);

JavaType responseType = OBJECT_MAPPER
        .getTypeFactory()
        .constructParametricType(TianYanChaResponse.class, listResultType);

TianYanChaResponse<TianYanChaListResult<TianYanChaResultItem190>> response =
        OBJECT_MAPPER.readValue(jsonStr, responseType);

核心思想就是:

二十一、TypeReference 和 JavaType 的区别

TypeReferenceJavaType 都是用来解决泛型擦除问题的。

它们的目的都是告诉 Jackson 完整的泛型类型。

但是使用场景不同。

1. TypeReference 适合固定泛型

例如:

复制代码
List<Map<String, TestRegion>> list = JsonUtil.string2Obj(
        jsonStr,
        new TypeReference<List<Map<String, TestRegion>>>() {}
);

这里的类型在写代码的时候已经确定了。

也就是说,代码里已经写死了:

复制代码
List<Map<String, TestRegion>>

这种情况使用 TypeReference 最直观。


2. JavaType 适合动态泛型

例如:

复制代码
public static <T> List<T> string2List(String str, Class<T> clazz)

这里的 T 不是写死的,而是调用方法时传进来的。

可能是:

复制代码
TestRegion.class
User.class
Order.class

这种情况就更适合用 JavaType

因为 JavaType 可以在运行时动态拼接出完整的泛型类型。

二十二、完整工具方法整理

1. 对象转 JSON 字符串

复制代码
public static <T> String obj2String(T obj) {
    if (obj == null) {
        return null;
    }
    try {
        return obj instanceof String ? (String) obj : OBJECT_MAPPER.writeValueAsString(obj);
    } catch (JsonProcessingException e) {
        log.warn("Parse Object to String error : {}", e.getMessage());
        return null;
    }
}

2. 对象转格式化 JSON 字符串

复制代码
public static <T> String obj2StringPretty(T obj) {
    if (obj == null) {
        return null;
    }
    try {
        return obj instanceof String ? (String) obj :
                OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
    } catch (JsonProcessingException e) {
        log.warn("Parse Object to String error : {}", e.getMessage());
        return null;
    }
}

3. JSON 字符串转普通对象

复制代码
public static <T> T string2Obj(String str, Class<T> clazz) {
    if (StringUtils.isEmpty(str) || clazz == null) {
        return null;
    }
    try {
        return clazz.equals(String.class) ? (T) str : OBJECT_MAPPER.readValue(str, clazz);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to Object error : {}", e.getMessage());
        return null;
    }
}

4. JSON 字符串转固定复杂泛型

复制代码
public static <T> T string2Obj(String str, TypeReference<T> valueTypeRef) {
    if (StringUtils.isEmpty(str) || valueTypeRef == null) {
        return null;
    }
    try {
        return OBJECT_MAPPER.readValue(str, valueTypeRef);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to Object error : {}", e.getMessage());
        return null;
    }
}

5. JSON 字符串转 List

复制代码
public static <T> List<T> string2List(String str, Class<T> clazz) {
    if (StringUtils.isEmpty(str) || clazz == null) {
        return null;
    }

    JavaType javaType = OBJECT_MAPPER
            .getTypeFactory()
            .constructParametricType(List.class, clazz);

    try {
        return OBJECT_MAPPER.readValue(str, javaType);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to Object error : {}", e.getMessage());
        return null;
    }
}

6. JSON 字符串转 Map<String, T>

复制代码
public static <T> Map<String, T> string2Map(String str, Class<T> valueClass) {
    if (StringUtils.isEmpty(str) || valueClass == null) {
        return null;
    }

    JavaType javaType = OBJECT_MAPPER
            .getTypeFactory()
            .constructMapType(LinkedHashMap.class, String.class, valueClass);

    try {
        return OBJECT_MAPPER.readValue(str, javaType);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to Object error : {}", e.getMessage());
        return null;
    }
}

7. JSON 字符串转 List<Map<String, T>>

复制代码
public static <T> List<Map<String, T>> string2ListMap(String str, Class<T> valueClass) {
    if (StringUtils.isEmpty(str) || valueClass == null) {
        return null;
    }

    JavaType mapType = OBJECT_MAPPER
            .getTypeFactory()
            .constructMapType(LinkedHashMap.class, String.class, valueClass);

    JavaType listType = OBJECT_MAPPER
            .getTypeFactory()
            .constructCollectionType(List.class, mapType);

    try {
        return OBJECT_MAPPER.readValue(str, listType);
    } catch (JsonProcessingException e) {
        log.warn("Parse String to List<Map<String, T>> error : {}", e.getMessage());
        return null;
    }
}

二十三、实际开发中怎么选择

可以按下面的规则选择:

复制代码
普通对象:
使用 Class<T>

固定复杂泛型:
使用 TypeReference<T>

动态 List<T>:
使用 JavaType + constructParametricType 或 constructCollectionType

动态 Map<String, T>:
使用 JavaType + constructMapType

动态 List<Map<String, T>>:
先构造 Map 的 JavaType,再构造 List 的 JavaType

再简单一点:

二十四、总结

Jackson 是一个功能强大的 JSON 处理框架。

普通对象转换比较简单,直接使用 Class<T> 就可以。

但是遇到泛型结构时,只写 List.classMap.class 是不够的,因为 Java 存在泛型擦除。

当 Jackson 不知道具体泛型时:

复制代码
JSON 的 {} 会默认转成 LinkedHashMap
JSON 的 [] 会默认转成 ArrayList

所以如果我们没有指定完整泛型类型,就容易出现:

复制代码
LinkedHashMap cannot be cast to Xxx

这类问题。

解决方式主要有两种:

复制代码
1. TypeReference
2. JavaType

TypeReference 适合固定的复杂泛型。

JavaType 适合运行时动态构造泛型。

最终可以记住一句话:

复制代码
普通对象用 Class,固定复杂泛型用 TypeReference,动态泛型用 JavaType。

在实际项目中,把这些方法封装到 JsonUtil 工具类中,可以减少重复代码,也可以避免因为泛型擦除导致的类型转换问题。

相关推荐
guslegend1 小时前
Java 创建对象有几种方式
java·开发语言
暗暗别做白日梦1 小时前
延时消息的几种实现方式及优缺点
java
极客先躯1 小时前
高级java每日一道面试题-2026年02月08日-实战篇[Docker]-如何实现容器的快照和恢复?
java·运维·docker·容器·备份·持久化·恢复
布朗克1681 小时前
29 反射机制
java·开发语言·反射
San813_LDD1 小时前
[数据结构]共享栈与双端队列:算法思想分析及C语言实现
java·开发语言·数据结构
我是一颗柠檬1 小时前
【Java项目技术亮点】全链路分层限流:从网关到数据库的多层防护体系
java·开发语言·数据库
wuminyu1 小时前
Java锁膨胀机制之偏向锁到轻量级锁源码剖析
java·linux·c语言·jvm·c++
码不停蹄的玄黓2 小时前
SpringBoot 实现拦截器
java·spring boot·后端
狗凯之家源码网2 小时前
永夜大圣 H5 棋牌大厅源码效果实测与品质解析
java·开发语言