目录
总结概括:
在实际项目开发中,有时候我们需要将实体类型数据转化为 Json 串,对方希望接收的 Json 数据中的 key 可能不是我们实体类定义的 key ,例如我们后端定义的是"createDate",但是对方接收的数据 key 希望是 "date";
还有一些时候,我们在对接第三方API的时候,对方返回的往往是 Json 数据,如果其中含有下划线,通常与Java 的实体类驼峰定义有差异,导致我们反序列化进行存表时,会有部分字段无法直接映射,需要在代码中进行xxx.setXXX(xxx.getXXX),简单来说就是将无法直接映射的,通过 get 方法获取出来再 set 到对应的实体对象中;
所以本篇文章就来简要说一说,如何通过注解来解决序列化、反序列化的字段映射问题。
一. 区别对比
下面表格是这两个注解的主要区别,小伙伴们主要关注"字段重命名","日期格式化","控制序列化方向"这三个就够了。
特性 | @JsonProperty (Jackson) | @JSONField (Fastjson) |
---|---|---|
所属库 | Jackson (Spring Boot默认集成) | Fastjson (阿里巴巴开源) |
字段重命名 | @JsonProperty("new_name") |
@JSONField(name = "new_name") |
控制序列化方向 | access 属性:READ_ONLY (只读)WRITE_ONLY (只写) |
serialize /deserialize 属性:serialize=false (不序列化)deserialize=false (不反序列化) |
日期格式化 | 需配合@JsonFormat :@JsonFormat(pattern="yyyy-MM-dd") |
直接支持:@JSONField(format="yyyy-MM-dd") |
默认值 | defaultValue 属性:@JsonProperty(defaultValue = "unknown") |
不支持 |
字段顺序 | 需配合@JsonPropertyOrder 注解 |
ordinal 属性:@JSONField(ordinal = 1) |
自定义序列化 | 通过@JsonSerialize 指定自定义序列化器 |
serializeUsing 属性:@JSONField(serializeUsing = MySerializer.class) |
二. 代码举例说明
2.1 准备实体类
如下定义了一个 Student 学生类,简单定义了几个属性,数据类型也是项目中较为常见的几种。
注解暂时都注释掉了,下面我们在做对应的注解测试的时候,把相对应的注解打开即可。
@Data
public class Student implements Serializable {
private static final long serialVersionUID = 20250627L;
// 主键ID
// @JsonProperty("id")
// @JSONField(name = "id")
private Long id;
// 学生姓名
// @JsonProperty("student_name")
// @JSONField(name = "student_name")
private String studentName;
// 学生年龄
// @JsonProperty("student_age")
// @JSONField(name = "student_age")
private Integer studentAge;
// 创建时间
// @JsonProperty("create_time")
// @JsonFormat(pattern = "yyyy-MM-dd") // @JsonFormat和@JsonFormat 都是Jackson的注解,要组合使用
// @JSONField(name = "create_time", format = "yyyy-MM-dd")
private Date createTime;
// 最高可接受的辅导价格(价格/小时)
// @JsonProperty("price")
// @JSONField(name = "price")
private BigDecimal price;
}
2.2 不加注释
public static void main(String[] args) throws JsonProcessingException {
// 不加注释测试
// 1. 创建对象
Student student = new Student();
student.setId(10001L);
student.setStudentName("张三");
student.setStudentAge(20);
student.setCreateTime(new Date()); // 当前时间
student.setPrice(new BigDecimal("150.50"));
System.out.println("使用对象: "+ student);
// 2. 使用FastJson包的方法 序列化为 json 字符串
String studentJsonStr = JSON.toJSONString(student);
System.out.println("使用FastJson包序列化为Json字符串: "+ studentJsonStr);
// 3. 使用FastJson包的方法 反序列化为对象
Student student2 = JSON.parseObject(studentJsonStr, Student.class);
System.out.println("使用FastJson包反序列化为对象: "+ student2);
// 4. 使用Jackson包 方法 序列化为 json 字符串
ObjectMapper objectMapper = new ObjectMapper();
String writeValueAsString = objectMapper.writeValueAsString(student);
System.out.println("使用Jackson包序列化为Json字符串: "+ writeValueAsString);
// 5. 使用Jackson包 方法 反序列化为对象
Student student3 = objectMapper.readValue(writeValueAsString, Student.class);
System.out.println("使用Jackson包反序列化为对象: "+ student3);
}
运行 main 方法,得出下图结果。
可以得出,在没有加入注解的时候,如果将实体类序列化为JSON串,时间对象会序列化为时间戳,当反序列化时,又会把时间戳序列化为 Date 类型对象。
此外,使用 FastJson 包序列化后,属性的顺序似乎发生了,变成了从 a~z 的顺序,不是原有实体类中定义的顺序,而 Jackson 包则保留了原有的类属性顺序;在反序列化时,则回到了原有的属性顺序,一个无用小芝士了解一下。
2.3 @JsonProperty
public static void main(String[] args) throws JsonProcessingException {
// @JsonProperty 注解测试
// 1. 创建对象
Student student = new Student();
student.setId(10001L);
student.setStudentName("张三");
student.setStudentAge(20);
student.setCreateTime(new Date()); // 当前时间
student.setPrice(new BigDecimal("150.50"));
System.out.println("使用对象: "+ student);
// 2. 使用Jackson包 方法 序列化为 json 字符串
ObjectMapper objectMapper = new ObjectMapper();
String writeValueAsString = objectMapper.writeValueAsString(student);
System.out.println("使用Jackson包序列化为Json字符串: "+ writeValueAsString);
// 3. 使用Jackson包 方法 反序列化为对象
Student student3 = objectMapper.readValue(writeValueAsString, Student.class);
System.out.println("使用Jackson包反序列化为对象: "+ student3);
}
运行 main 方法,通过结果不难发现,我们添加的注解已经起作用了,序列化为 json 的时候,变成了下划线,并且日期也进行了格式化,不再是时间戳。
然后,我们再来做一步,将 json 数据做修改,让它重新序列化
public static void main(String[] args) throws JsonProcessingException {
// @JsonProperty 注解测试
// 1. 创建对象
Student student = new Student();
student.setId(10001L);
student.setStudentName("张三");
student.setStudentAge(20);
student.setCreateTime(new Date()); // 当前时间
student.setPrice(new BigDecimal("150.50"));
System.out.println("使用对象: "+ student);
// 2. 使用Jackson包 方法 序列化为 json 字符串
ObjectMapper objectMapper = new ObjectMapper();
String writeValueAsString = objectMapper.writeValueAsString(student);
System.out.println("使用Jackson包序列化为Json字符串: "+ writeValueAsString);
// 修改序列化后 json 字符串,将 key student_name 改为 studentName;去除 key student_age,添加另外一个 key student_sex
// 3. 使用Jackson包 方法 反序列化为对象
String json = "{\"id\":10001,\"studentName\":\"张三\",\"student_sex\":\"男\",\"create_time\":\"2025-06-27\",\"price\":150.50}";
Student student3 = objectMapper.readValue(json, Student.class);
System.out.println("修改后的json字符串使用Jackson包反序列化为对象: "+ student3);
}
运行 main 方法,可以发现,竟然报错了,说无法识别 "studentName" 这个属性,因为我们实体类中 studentName 上的注解是 @JsonProperty("student_name"),与 json 中的 key studentName 不相等没导致无法映射报错。
其实想要解决也很简单,我们需要在实体类上再添加一个注解
注解的意思就是在转化过程中,忽略位置的属性(无法正确映射的属性)
@JsonIgnoreProperties(ignoreUnknown = true)
然后我们再重新运行方法,如下结果所示,可以发现,因为 studentName 无法映射和 student_age 的缺失,导致这两个属性没有反序列化到实体类的属性值中,而且多余的键 "student_sex" 丢失,没有映射到实体类中。
2.4 @JSONField
然后我们将实体类的 @JSONField 注解打开
public static void main(String[] args) throws JsonProcessingException {
// @JSONField 注解测试
// 1. 创建对象
Student student = new Student();
student.setId(10001L);
student.setStudentName("张三");
student.setStudentAge(20);
student.setCreateTime(new Date()); // 当前时间
student.setPrice(new BigDecimal("150.50"));
System.out.println("使用对象: "+ student);
// 2. 使用FastJson包的方法 序列化为 json 字符串
String studentJsonStr = JSON.toJSONString(student);
System.out.println("使用FastJson包序列化为Json字符串: "+ studentJsonStr);
// 3. 使用FastJson包的方法 反序列化为对象
Student student2 = JSON.parseObject(studentJsonStr, Student.class);
System.out.println("使用FastJson包反序列化为对象: "+ student2);
}
运行上述方法,从结果可以看出,添加了@JSONField 注解后。
在序列化为 Json 字符串之后,key 都变成了我们注解中标注的带有下划线的值了,并且日期格式化也生效了,没有转化为时间戳,而是直接日期。
在进行反序列化时,带有下划线格式的 Json ,通过注解的映射,将数据成功反序列化为实体对象。
这里我们来多做一步,纂改一下 Json 串的数据,或者说我们不使用序列化之后的字符串去进行反序列化,自定义一个 json 串。
public static void main(String[] args) throws JsonProcessingException {
// @JSONField 注解测试
// 1. 创建对象
Student student = new Student();
student.setId(10001L);
student.setStudentName("张三");
student.setStudentAge(20);
student.setCreateTime(new Date()); // 当前时间
student.setPrice(new BigDecimal("150.50"));
System.out.println("使用对象: "+ student);
// 2. 使用FastJson包的方法 序列化为 json 字符串
String studentJsonStr = JSON.toJSONString(student);
System.out.println("使用FastJson包序列化为Json字符串: "+ studentJsonStr);
// 3. 使用FastJson包的方法 反序列化为对象
// 修改Json 串,将 key create_time 改为 createTime,将 key student_name 删除,添加一个新 key student_sex
String json = "{\"createTime\":\"2025-06-27\",\"id\":10001,\"price\":150.50,\"student_age\":20,\"student_sex\":\"男\"}";
Student student2 = JSON.parseObject(json, Student.class);
System.out.println("修改后反序列化为对象: "+ student2);
}
运行方法,得出结果
不难发现,
修改1:虽然修改了 json 中的 key,将create_time 改为 createTime,和注解@JSONField(name = "create_time")不同,但是 方法还是智能识别(1.2.83版本后似乎均有此功能,小编没有一个个尝试)并进行了映射,日期值仍然成功反序列化到了实体类的 createTime 属性上;
修改2:由于去掉了 student_name 这个 key,导致反序列化时无值且无映射,导致得到的实体对象中 studentName 值为 null;
修改3:虽然在 json 串中添加了 student_sex 这个年龄 key,但是实体类中没有相关的字段进行接收,所以得到实体类没有这个属性,这个 key 实际上会在反序列化时舍弃。
三. 全局映射解决方案
3.1 fastjson 包
fastjson 不是 Spring Boot 默认的JSON处理器,所以通常需要在代码中进行配置;
小编个人建议如果有自定义配置或全局配置,可以在 @Configuration 配置类中配置,参考如下代码
@Configuration
public class FastjsonConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 1. 创建 FastJson 消息转换器
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
// 2. 创建配置类
FastJsonConfig config = new FastJsonConfig();
// 全局驼峰转下划线
config.setSerializerFeatures(SerializerFeature.PrettyFormat);
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
// 3. 将配置注入转换器
converter.setFastJsonConfig(config);
// 4. 添加到转换器列表并优先使用
converters.add(converter);
}
}
3.2 jackson 包
因为 jcakson 是Spring Boot 默认的JSON处理器,所以它的配置支持使用 yml 文件,同时也支持@Configuration 配置文件。如果 yml 文件和 Configuration 同时配置,则 Configuration 优先级更高;
方式一:yml 文件(推荐方案)
优点:更加灵活,无需代码;
缺点:自定义序列化无法实现,仍需使用配置文件。
spring:
jackson:
property-naming-strategy: SNAKE_CASE # 驼峰转下划线
date-format: yyyy-MM-dd HH:mm:ss # 日期格式
fail-on-unknown-properties: false # 忽略未知字段
time-zone: GMT+8 # 时区
方式二:配置文件
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
}
四. Serializable 接口(多学一招)
细心的小伙伴可以发现,在绝大多数实体类中,都实现了 Serializable 接口,并且通常都会定义一个序列化版本号。
private static final long serialVersionUID = 1L;
那么 Serializable 是否和 fastjson、jackson 有什么关联呢?
这里可以给个问小伙伴解答一下,Serializable 接口与后两者没有直接关联。
实体类实现 Serializable 接口,主要是为了支持Java原生序列化机制,而 serialVersionUID 用于版本控制。
而且,如果需要用到 ObjectInputStream、ObjectOutputStream、HttpSession 存储等操作,必须实现 Serializable 接口!!!
此外,实现 Serializable 接口 的好处还有以下几点,大家可以参考一下
版本控制机制:兼容新老数据,旧数据可被新版本反序列化,新版本也可被旧版本反序列化;
避免隐式UID风险:环境不一致可能导致开发/生产环境的JVM差异导致序列化失败;
若修改类字段,类版本变更,可能导致反序列化后抛出 InvalidClassException 无效的类异常
显式UID的优势:
场景 | 显式 UID | 隐式 UID |
---|---|---|
新增非关键字段 | 兼容 | 不兼容 |
修改字段顺序 | 兼容 | 不兼容 |
跨 JVM 供应商部署 | 兼容 | 可能不兼容 |
重构方法(不影响字段) | 兼容 | 不兼容 |
总而言之,以后各位小伙伴在做需求新建表,新建实体类的时候,无脑加上 Serializable 接口的实现 和 private static final long serialVersionUID = 1L 就行。