在前后端交互和第三方接口对接中,把一段 JSON 字符串数组转换成 List 集合几乎是每天都会遇到的场景。看起来只是调用一行 API 的事情,实际隐藏着不少坑:键名风格不一致导致字段全为 null、泛型擦除导致强转报错、数字精度丢失等等。本文以最常用的 Jackson 和 fastjson 为例,讲清楚如何安全地把 JSON 字符串数组转成 List,并重点解决下划线键名到驼峰属性的映射问题。

一、先理解泛型擦除带来的坑
很多初学者会写出这样的代码:
ObjectMapper mapper = new ObjectMapper(); List<User> users = (List<User>) mapper.readValue(json, List.class);
编译能通过,运行时一用 users.get(0).getName() 就抛 ClassCastException。原因在于 readValue(json, List.class) 只知道要生成一个 List,不知道里面的元素该映射成什么类型,Jackson 会把每个 JSON 对象反序列化成 LinkedHashMap 塞进 List。此时 List 的实际泛型是 List<LinkedHashMap>,强转成 List<User> 在编译期被擦除检查放过,运行期必然报错。
正确的做法是显式告诉反序列化器完整的泛型信息。Jackson 提供了 TypeReference,fastjson 也提供了同名工具,两者用法类似:
ObjectMapper mapper = new ObjectMapper();
List<User> users = mapper.readValue(
jsonArrayString,
new TypeReference<List<User>>() {}
);这里的匿名内部类 new TypeReference<List<User>>() {} 会把泛型签名保留在字节码里,框架通过反射读取这个签名就能还原出完整的 List<User> 结构。如果目标类型是运行时才确定的,还可以用 JavaType 动态构造:
JavaType javaType = mapper.getTypeFactory()
.constructCollectionType(List.class, User.class);
List<User> users = mapper.readValue(jsonArrayString, javaType);二、下划线键名如何映射到驼峰属性
对接第三方接口时,返回的 JSON 经常是下划线风格,比如 {"user_name":"张三","create_time":"2024-01-01"},而 Java 实体遵循驼峰命名 userName、createTime。如果不做任何配置,Jackson 默认按字段名精确匹配,找不到就忽略,转换出来的对象字段全是 null,而且不会报任何错误,这种静默失败在排查问题时非常隐蔽。
解决方式有两种。第一种是全局配置命名策略,让框架自动做下划线与驼峰的互转:
ObjectMapper mapper = new ObjectMapper(); mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
注意这个策略是双向的:反序列化时把 user_name 映射到 userName,序列化时也会把 userName 输出成 user_name。如果只需要在某个实体上生效,更推荐第二种方式,使用注解精确控制:
public class User {
@JsonProperty("user_name")
private String userName;
@JsonProperty("create_time")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
// 省略 getter/setter
}注解方式的优点是不影响全局行为,多个不同风格的接口可以共存;缺点是字段多时写起来繁琐。实际项目中建议:自己系统内部的接口用全局策略,对接外部接口的 DTO 用注解,两者互不干扰。fastjson 用户对应的做法是在字段上加 @JSONField(name = "user_name"),或在解析时用 Feature.SupportSmartMatch,思路一致。
三、封装一个安全的转换工具类
直接在业务代码里 new ObjectMapper 既浪费对象开销,也可能因为各处配置不一致带来行为差异。建议把转换逻辑收敛到一个工具类中,统一配置、统一异常处理:
public class JsonUtils {
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
// 反序列化时忽略未知字段,提升兼容性
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 空字符串转 null,避免抛错
MAPPER.coercionConfigFor(String.class)
.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
}
public static <T> List<T> parseList(String json, Class<T> clazz) {
if (json == null || json.isBlank()) {
return Collections.emptyList();
}
try {
CollectionType type = MAPPER.getTypeFactory()
.constructCollectionType(List.class, clazz);
return MAPPER.readValue(json, type);
} catch (JsonProcessingException e) {
throw new IllegalStateException("JSON 转 List 失败: " + e.getOriginalMessage(), e);
}
}
}这个工具类有几个细节值得注意。第一,ObjectMapper 是线程安全的,声明为静态常量全局复用即可,不需要每次创建。第二,FAIL_ON_UNKNOWN_PROPERTIES 关闭后,JSON 里出现实体类没有的字段不会报错,这对接口字段演进的兼容性很重要。第三,入参判空直接返回空集合而不是 null,调用方拿到后可以直接遍历,避免空指针。
调用方式非常简单:
String json = "[{\"user_name\":\"张三\",\"create_time\":\"2024-01-01 12:00:00\"}]";
List<User> users = JsonUtils.parseList(json, User.class);
users.forEach(u -> System.out.println(u.getUserName()));四、边界情况与常见误区
除了映射问题,还有几个容易踩的点需要提前考虑。其一是日期格式,Jackson 默认只认时间戳和 ISO 格式,遇到 2024-01-01 12:00:00 这种格式要加 @JsonFormat 或全局注册 JavaTimeModule 并设置日期格式。其二是数字精度,JSON 里超过 Long 范围的整数如果映射到 Double 会丢精度,金融场景建议字段直接用 BigDecimal 接收。其三是元素类型不一致,比如数组里混了字符串和对象,可以在转换前用 JsonNode 先校验一遍结构:
JsonNode root = MAPPER.readTree(json);
if (!root.isArray()) {
throw new IllegalArgumentException("入参不是 JSON 数组");
}
for (JsonNode node : root) {
if (!node.isObject()) {
throw new IllegalArgumentException("数组中存在非对象元素");
}
}最后是框架选择问题。fastjson 的 parseArray 用法更简短,但历史上出现过多个高危漏洞版本,如果使用请务必升级到 fastjson2 或确认版本已修复已知问题;Jackson 功能更完整、社区更活跃,是 Spring Boot 的默认选择。无论选哪个,核心思路都一样:显式传递泛型信息、明确键名映射策略、关闭过度严格的默认校验、对异常输入做兜底处理。把这四点做到位,JSON 字符串数组转 List 这件事就真正安全了。
JSON转换List驼峰映射Jackson修改时间:2026-09-08 18:42:54