栏目:注解速查 | 环境:Spring Boot 3.2.7 | JDK 21 | Jackson 2.15.4
后端接口返回的 JSON,前端经常挑毛病:字段名是驼峰、前端要的是下划线;日期变成数组或时间戳;null 字段刷屏;更糟的是 password 这种敏感字段直接暴露了出去。这些问题都能在 DTO 上用 5 个 Jackson 注解解决。
本文用一个用户详情对象演示"默认序列化"的问题,再逐个加上 5 个注解。所有 JSON 输出都是本机运行的真实结果,最后附速查表。
环境信息
- JDK 21 + Spring Boot 3.2.7(内置 Jackson 2.15.4)
- 依赖:
jackson-databind+jackson-datatype-jsr310(Spring Boot 项目里spring-boot-starter-web已带) - 验证:JUnit 5 + 一个 main 程序;本文所有 JSON 均来自本地实测输出
第一步:不加注解时,JSON 长什么样
先看一个"裸"对象 RawUserProfile(字段:id、userName、password、nickname、email、createdAt、address),直接序列化:
java
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
System.out.println(mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(rawUserProfile));
真实输出:
json
{
"id" : 1001,
"userName" : "coder_liu",
"password" : "123456",
"nickname" : null,
"email" : "liu@example.com",
"createdAt" : [ 2026, 8, 11, 10, 0 ],
"address" : {
"city" : "深圳",
"street" : "科技园南区"
}
}
四个问题一眼就能看出来:
userName是驼峰,前端如果约定下划线就得自己再转一遍;- password 直接泄露,这是最危险的一条;
nickname为 null 也输出了,字段一多整个 JSON 全是 null;createdAt输出成数组[2026, 8, 11, 10, 0]------这是 LocalDateTime 在默认配置下的样子,对前端既不直观也不好解析。
第二步:@JsonProperty------字段重命名
前端约定字段是 user_name,后端 Java 里习惯写 userName。改名靠 @JsonProperty:
java
@JsonProperty("user_name")
private String userName;
注解后,序列化输出 "user_name" : "coder_liu";反序列化时收到 user_name 也会自动填回 userName 字段------双向生效。
@JsonProperty 还有一个 access 属性控制读写方向,比如"只收不发",放到第七步一起讲。
第三步:@JsonIgnore------敏感字段不输出
password 这种字段压根不该出现在接口返回里:
java
@JsonIgnore
private String password;
加完注解,序列化输出里 password 直接消失。注意一个容易被忽略的点:@JsonIgnore 是双向的 ------反序列化时同样忽略这个字段。实测里我故意在输入 JSON 里塞了 "password":"hacked",反序列化后 password 仍然是 null,前端传进来的值被静默丢弃。
如果你的需求是"返回时隐藏、接收时允许传入"(比如修改密码接口),@JsonIgnore 就不合适,改用
@JsonProperty(access = Access.WRITE_ONLY),见第七步。
第四步:@JsonInclude------null 字段别刷屏
可空字段(昵称、邮箱)没填时是 null,默认会原样输出。用 @JsonInclude 控制:
java
@JsonInclude(JsonInclude.Include.NON_NULL)
private String nickname;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String email;
Include.NON_NULL:值为 null 就不输出。还有两个常用值:
NON_EMPTY:null、空字符串、空集合都不输出;NON_DEFAULT:值和默认值(如 0、false)一样时不输出。
怎么选:字符串类字段用 NON_NULL 就行,集合字段推荐 NON_EMPTY,避免输出一堆 []。
第五步:@JsonFormat------日期格式与时区
把 LocalDateTime 输出成前端友好的字符串,并固定时区:
java
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createdAt;
输出变成:
json
"createdAt" : "2026-08-11 10:00:00"
这里有个高频坑:@JsonFormat 不写 timezone 时,Date 类型默认按 UTC 序列化。 我用同一个时间戳做了个对比实验:
java
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
public Date defaultTz; // 没写 timezone
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
public Date beijingTz; // 写了 timezone
真实输出:
json
{
"defaultTz" : "2026-08-11 02:00:00",
"beijingTz" : "2026-08-11 10:00:00"
}
同一个时刻,没写 timezone 的字段比北京时间早了 8 个小时 。所以凡是 Date 字段,@JsonFormat 里都建议显式写 timezone = "GMT+8"(或 Asia/Shanghai)。
另外两个知识点:
- LocalDateTime 本身没有时区概念,格式化用的是字段里存的值;时区坑主要出在
java.util.Date上; - LocalDateTime 能被序列化,依赖
JavaTimeModule。Spring Boot 项目自动配好了;本文 Demo 是裸 ObjectMapper,所以手动registerModule(new JavaTimeModule())。
第六步:@JsonUnwrapped------嵌套对象展开到顶层
address 对象只有两个字段,前端不想要多一层嵌套:
java
@JsonUnwrapped
private Address address;
序列化后 address 的字段直接平铺到顶层:
json
"city" : "深圳",
"street" : "科技园南区"
反序列化同样支持:前端传 city、street,会自动组装回 address 对象。实测回读:
text
userName = coder_liu
address = Address[city=深圳, street=科技园南区]
注意:@JsonUnwrapped 适合简单扁平的对象;如果嵌套对象字段很多、或者要配合构造器 / Lombok Builder 使用,建议先单独验证再上生产。
第七步:@JsonProperty 的 access------只收不发
场景:接口接收一个 login_count 入参,但响应里不回传:
java
@JsonProperty(value = "login_count", access = JsonProperty.Access.WRITE_ONLY)
private int loginCount;
实测:
text
序列化输出(不应包含 login_count):
{"id":1001,"createdAt":"2026-08-11 10:00:00","city":"深圳","street":"科技园南区","user_name":"coder_liu"}
反序列化输入:{"id":1001,"login_count":5,"password":"hacked"}
loginCount = 5(收下了)
password = null(被忽略,未注入)
WRITE_ONLY 表示"只允许写入(反序列化),不参与输出"。和 @JsonIgnore 正好互补:
- @JsonIgnore:双向忽略,输出、输入都不管;
@JsonProperty(access = WRITE_ONLY):输入收、输出不收;@JsonProperty(access = READ_ONLY):输出给、输入不收(适合有计算逻辑、不想让前端传入的字段)。
完整注解版(组合案例)
把所有注解加到同一个类上,就是 Demo 里的完整 UserProfile:
java
public class UserProfile {
private Long id;
@JsonProperty("user_name")
private String userName;
@JsonIgnore
private String password;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String nickname;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String email;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createdAt;
@JsonUnwrapped
private Address address;
@JsonProperty(value = "login_count", access = JsonProperty.Access.WRITE_ONLY)
private int loginCount;
// getter / setter 略,完整代码见 Demo
}
最终序列化输出:
json
{
"id" : 1001,
"createdAt" : "2026-08-11 10:00:00",
"city" : "深圳",
"street" : "科技园南区",
"user_name" : "coder_liu"
}
对比第一步的默认输出:敏感字段没了、null 没了、日期可读了、嵌套展开了、字段名也按前端约定来了。
总结:3 个 Key Takeaways
- 默认 JSON 有 4 个典型问题(字段名、敏感字段、null、日期),5 个注解解决这些问题;
- Date 字段的 @JsonFormat 一定要写 timezone,否则按 UTC 输出会差 8 小时;
- @JsonIgnore 双向忽略 ,要"只收不发"用
@JsonProperty(access = WRITE_ONLY)。
行动项:打开你的项目,检查返回前端的 DTO------有没有 password 这类敏感字段、有没有一堆 null、日期是不是还是数组或时间戳。有就按下面的速查表逐个改。
注解速查表
| 注解 | 作用 | 常用属性 |
|---|---|---|
| @JsonProperty | 字段重命名、控制读写方向 | value / access |
| @JsonIgnore | 序列化与反序列化都忽略字段 | --- |
| @JsonInclude | 控制空值是否输出 | value(NON_NULL / NON_EMPTY / NON_DEFAULT) |
| @JsonFormat | 字段格式(日期、数字) | pattern / timezone / shape |
| @JsonUnwrapped | 嵌套对象展开到顶层 | prefix / suffix |