Jackson 序列化:@JsonIgnore / @JsonProperty / @JsonFormat / @JsonInclude / @JsonUnwrapped 一次讲清楚

栏目:注解速查 | 环境: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" : "科技园南区"
  }
}

四个问题一眼就能看出来:

  1. userName 是驼峰,前端如果约定下划线就得自己再转一遍;
  2. password 直接泄露,这是最危险的一条;
  3. nickname 为 null 也输出了,字段一多整个 JSON 全是 null;
  4. 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" : "科技园南区"

反序列化同样支持:前端传 citystreet,会自动组装回 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

  1. 默认 JSON 有 4 个典型问题(字段名、敏感字段、null、日期),5 个注解解决这些问题;
  2. Date 字段的 @JsonFormat 一定要写 timezone,否则按 UTC 输出会差 8 小时;
  3. @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
相关推荐
码路漫漫4 小时前
用了 Caffeine,消息为什么还是被处理了两次?
java
云和数据.ChenGuang4 小时前
fastapi项目拆分实战数据模型
java·服务器·数据库·人工智能·深度学习·fastapi·强化学习
雨落倾城夏未凉4 小时前
halcon核心-图像预处理(四)
后端
用户233376852184 小时前
一张损坏JPEG文件的背后排查
后端
nnerddboy5 小时前
Rust教程06:ESP32-rust环境搭建
开发语言·后端·rust
步行cgn5 小时前
MyBatis resultMap 结果映射完全指南
后端
nnerddboy5 小时前
Rust教程03:函数,控制流与所有权
开发语言·后端·rust
小龙报5 小时前
【优选算法】1.搜索插入位置 2.x的平方根
java·c语言·数据结构·c++·python·算法·蓝桥杯
山荷枝5 小时前
04-框架--SpringBoot
java·spring boot·后端