资讯动态

Java后端必备:@JsonProperty与@JsonInclude注解实战解析

发布时间:2026/10/21 14:04:53 来源:尧图企业网站定制
不想再看到前后端因为字段名撕逼的场面不想让接口返回里塞满一堆没用的null和空数组JsonProperty和JsonInclude这俩注解就是干这个用的。做 Java 后端天天跟 Jackson 打交道这两个注解是绕不过去的核心工具。一个管“字段叫什么名字”一个管“字段什么时候出现”搞懂了它们你写出来的接口不光干净还能省掉不少跟前端对字段名的口水战。这篇文章不整虚的直接把这两个注解的参数、坑、配合使用场景全部扒开讲透。你用 Spring Boot 也好纯 Java 项目也罢只要序列化走的是 Jackson内容都通用。看完你至少能解决这几个问题Java 字段和 JSON 名字对不上怎么搞反序列化时如何兼容多个不同的字段名null、空字符串、空集合到底该不该返回全局配置和局部注解打架了听谁的1. 先从序列化和反序列化说起很多人用这两个注解只知道“加上就行”但不知道为什么加、什么时候加、加了为什么会生效。在进入注解细节之前非常有必要把 Jackson 处理 JSON 的底层逻辑捋一遍。Jackson 把 Java 对象转成 JSON 的过程叫做序列化。这个过程的本质是 Jackson 去“读取”一个 Java 对象的状态然后按照自己的规则把状态写成 JSON 字符串。它读什么优先读 gettergetXxx()或isXxx()没有 getter 就找字段再不行就找 setter 或者构造器参数。反过来把 JSON 字符串变成 Java 对象叫做反序列化Jackson 优先通过 setter 或者带参构造器去赋值没有就把值直接映射到字段上。这里面的关键点在于Jackson 默认使用Java 字段名作为 JSON 的 key。比如你有个字段叫lastLoginTime序列化出来 JSON 里就是lastLoginTime。这本身没问题问题出在现实世界里。前端那边可能管这个字段叫last_login_time另一个老接口可能叫lastLogin第三方平台给你返回的可能是LAST_LOGIN_TIME。字段名一不对齐数据就接不上。JsonProperty就是那个“对齐工具”它能在序列化和反序列化时让 Java 字段名和 JSON 字段名脱离绑定关系各叫各的。再说序列化时 Jackson 输出哪些字段。默认情况下只要你这个对象有值哪怕是nullJackson 也会把null原样输出。字段多的时候JSON 会变得特别冗长前端还要在代码里不断地判! null。JsonInclude的作用就是给字段设置“出场门槛”比如null不输出空集合不输出默认值不输出。这两个注解一个解决“名字对不对得上”一个解决“内容好不好看”在实际项目中常常是配合使用的。2.JsonProperty的核心功能与应用细节JsonProperty是com.fasterxml.jackson.annotation包下的注解它可以标注在字段、getter、setter 或者构造器参数上。下面把它的几个核心参数分开讲每个参数会有对应的代码示例。2.1 用value属性重命名字段名这是最常见的用法。假设有个用户对象Java 字段叫realName但接口文档里要求返回的是name。public class UserInfo { JsonProperty(name) private String realName; JsonProperty(userId) private Long id; private Integer age; }序列化之后JSON 长这样{ name : 张三, userId : 1001, age : 28 }反序列化时同样有效。前端给你传一个{name:李四,userId:1002}Jackson 也能正确地把name的值塞进realName字段、userId的值塞进id字段。这个注解还有个隐藏功能当字段是private且类上没有JsonAutoDetect时给字段加JsonProperty也能强制让 Jackson 认这个字段。相当于你给了 Jackson 一个明确的指示这个字段是要参与序列化的。有些老项目里不喜欢写 getter/setter用这个注解就能把需要暴露的字段显式标记出来。实操中的注意事项value属性不要用奇怪的字符。一般用驼峰、下划线或短横线前端能接受就行。不要用空格、引号这类非 JSON 安全字符。类名上如果配了JsonNaming策略比如SnakeCaseStrategy这时候手动加的JsonProperty(xxx)优先级更高手动指定的名字不会被全局命名策略覆盖。2.2 用alias属性兼容多个历史字段名有些老系统迭代了很多年同一个含义的字段在不同版本接口里有不同叫法。比如之前叫phone后来升级成了mobile但还有老客户端在传phone。反序列化时你只写一个JsonProperty只能认一个名字这时候就该用alias。public class UserContact { JsonProperty(mobile) JsonAlias({phone, telephone}) private String mobile; }这样传mobile、phone、telephone任意一个都能映射到mobile字段上。注意一点alias只在反序列化时生效序列化时永远只会输出value指定的名字。这很合理总不能让人家接口返回三个一样的字段吧。有个小陷阱要提醒如果同一个 JSON 里同时出现了mobile和phone两个字段Jackson 会以最后一个被解析到的为准。代码逻辑里千万别写出“JSON 里同时传两个字段我取非空的那个”这种不靠谱的依赖。2.3 用access属性控制读写权限某些场景下字段只允许输出不允许输入。比如用户表的createTime创建时间一般是服务端自己生成的前端传什么都没用再比如一些内部状态字段服务端要返回给前端看但绝不能接受前端传进来的值。这时候用access来限制读写方向。public class OrderInfo { JsonProperty(value orderId, access JsonProperty.Access.READ_ONLY) private String orderId; JsonProperty(value userRemark, access JsonProperty.Access.WRITE_ONLY) private String remark; }READ_ONLY的意思是这个字段只能“读”序列化输出不能“写”反序列化接收。前端往你接口传orderId会被忽略掉这样能防止用户篡改订单号。WRITE_ONLY相反只能接收前端传值序列化时不输出。某些敏感字段比如内部备注、内部处理人就可以用WRITE_ONLY保证不出现在接口返回里。有一种情况需要特别小心如果一个字段你既想让它输出又不想让它接收用READ_ONLY没问题。但有少数开发者会把READ_ONLY和setter 缺失搞混以为加了READ_ONLY之后传给这个字段的值就不报错了。实际上如果 JSON 里存在未知字段能否忽略不报错取决于全局的FAIL_ON_UNKNOWN_PROPERTIES配置和JsonProperty没有直接关系。2.4 用index和required控制顺序与必填index属性可以控制字段在序列化输出时的顺序数字小的先输出。虽然这在日常开发中用得比较少但如果你有“字段顺序影响签名校验”或者“需要按固定顺序输出给老旧系统”的需求它就有用了。public class ApiResponse { JsonProperty(value code, index 1) private int code; JsonProperty(value msg, index 2) private String msg; JsonProperty(value data, index 3) private Object data; }required属性我在实际项目中几乎不用它并不是强制校验字段有没有传而只是在反序列化时如果没有这个字段就报一个更明确的错误信息。字段是否必填应该在业务代码里做校验不能指望一个注解帮你挡掉所有非法请求。3.JsonInclude的过滤策略与使用场景JsonInclude解决的核心问题是序列化时什么条件下的字段要被输出什么条件下要被忽略。它能标在类上也能标在具体字段上。3.1 五个常用 Include 策略逐个说Include.ALWAYS无论字段什么值都输出。这就是默认行为null也输出。Include.NON_NULL字段为null时不输出。Include.NON_EMPTY字段为null、空字符串、空集合、空数组、空 Optional 时都不输出。Include.NON_DEFAULT字段值等于 Java 类型的默认值时比如int的 0、boolean的false、引用类型的null不输出。Include.USE_DEFAULT比较少用表示按全局默认配置来。注意如果你用的是Include.NON_DEFAULT对int类型字段来说值为0就不会被输出。前端如果恰好需要判断“数量是否为 0”把 0 丢掉可能引发业务判断问题。JsonInclude(JsonInclude.Include.NON_NULL) public class Product { private String skuId; private String name; private String description; }如果description是null输出结果只有skuId和name。这个策略在接口返回里最常用可以大大压缩 JSON 体积也减少前端判空的负担。NON_EMPTY比NON_NULL更激进一点它把空字符串也过滤掉了。有些前端框架对“字段不存在”和“字段为空串”的处理逻辑不一样你可能需要保留空串那就慎用NON_EMPTY。我自己做过一个对接老系统的项目对方要求每个字段都必须返回哪怕为空这时候用INCLUDE.ALWAYS反而更合适。所以选哪个策略要看你接口的消费端是什么风格不能一刀切。3.2 注解放在字段上还是类上效果不一样类上加了JsonInclude对类里所有字段生效。单个字段上再加字段级覆盖类级。JsonInclude(JsonInclude.Include.NON_NULL) public class UserVO { private Long id; private String name; JsonInclude(JsonInclude.Include.ALWAYS) private String nickname; }类级要求所有null都不输出但是nickname这个字段强制输出哪怕它是null。这种场景多用在“某些字段前端必须有值哪怕你给个 null 占位也行”的接口设计中。字段级注解的优先级高于类级这条规则要记牢。还有一点容易忽略JsonInclude可以用在Map 类型字段的 value 上用的是content属性。比如你有一个MapString, Object字段希望 map 中 value 为 null 的那些键值对不要输出写法是这样的public class ExtInfo { JsonInclude(value JsonInclude.Include.NON_NULL, content JsonInclude.Include.NON_NULL) private MapString, Object properties; }不加content的话JsonInclude只对 Map 本身这个字段生效比如 Map 为 null 时不输出不会管 Map 内部的键值对。加了content Include.NON_NULL之后value 为 null 的键值对会被过滤掉。这个细节很多人不知道处理动态扩展字段时能帮你挡掉不少坑。3.3 全局配置与注解配置的覆盖关系如果你用的是 Spring Boot完全可以在配置文件里给全局的 Jackson 设置一个默认的non-null策略这样不用在每个类上都累赘地加注解。spring: jackson: default-property-inclusion: non_null全局配置生效后所有接口返回值里为null的字段都会被忽略。但如果某个类上明确写了JsonInclude(JsonInclude.Include.ALWAYS)或者某个字段级别写了其他值注解会覆盖全局配置。如果用的是非 Spring 项目手动构建ObjectMapper时也可以设置ObjectMapper objectMapper new ObjectMapper(); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);优先级从高到低字段级注解 类级注解 ObjectMapper 全局配置 默认行为。这四条链路的优先级必须记牢否则排查“明明全局配了 NON_NULL为什么会话里冒出大量 null”这类问题时会绕很大的弯子。4. 这两个注解组合使用的经典实战场景单独理解每个注解容易难的是在真实项目中知道该在哪个位置组合使用。这一节列举我在实际项目中遇到的几类典型场景每个都能直接照着改。4.1 场景一对外 VO 重命名字段并过滤无意义值后端有个内部领域模型字段名是accountBalance含义是“账户余额”前端协议里叫balance。同时有些内部字段比如deletedFlag不应该出现在对外接口里而memo为 null 时也不需要输出。JsonInclude(JsonInclude.Include.NON_NULL) public class AccountVO { JsonProperty(balance) private BigDecimal accountBalance; JsonProperty(value internalStatus, access JsonProperty.Access.READ_ONLY) private Integer deletedFlag; private String memo; }这个组合很典型JsonProperty负责对外改名让内部字段结构和协议结构各干各的互不影响JsonInclude负责把所有为null的可选项全部丢掉access负责把敏感字段做成只输出不可接收。这类 VO 建议放在vo或dto包里不要直接拿实体类做接口返回不然改字段名的时候会带出一堆连锁问题。4.2 场景二对接第三方回调时兼容多种字段名第三方平台的回调消息里同一个业务 ID 在不同环境下叫法不同。有叫orderNo的有叫outTradeNo的还有叫bizOrderId的。你在反序列化时想要一个 Java 字段搞定所有情况。public class CallbackPayload { JsonProperty(orderNo) JsonAlias({outTradeNo, bizOrderId}) private String orderNo; JsonProperty(value notifyType, access JsonProperty.Access.WRITE_ONLY) private String notifyType; }value指定了你期望的标准名字alias则把历史遗留的其他名字也一并兼容。notifyType用WRITE_ONLY可以只在接收时生效不会在你主动给第三方发消息时把这个字段带出去避免产生循环引用或多余的上下文字段。对接外部系统时还有一种常见问题JSON 里某个字段在 Java 对象中不存在默认 Jackson 会抛异常。要兼容这种情况你得在全局配置里关掉FAIL_ON_UNKNOWN_PROPERTIES或者直接在类上加JsonIgnoreProperties(ignoreUnknown true)。这和JsonProperty是两码事但经常在同一个场景里被提到顺带说一句防止踩坑。4.3 场景三校验字段是否真的“被忽略”很多人以为JsonProperty(access Access.READ_ONLY)是把该字段从序列化里拿掉其实不是。它是“可以输出但不能接收”。真正要从序列化里彻底拿掉的注解是JsonIgnore二者经常被搞混。public class InternalData { JsonProperty(value secretKey, access JsonProperty.Access.WRITE_ONLY) private String secretKey; JsonIgnore private String innerTrace; }secretKey能接收前端传入的值但响应里永远看不到。innerTrace则完全从 JSON 序列化和反序列化中消失。你要做的判断标准是字段是“不想给别人看”用WRITE_ONLY或者JsonIgnore还是“别人传了也不该影响我”用READ_ONLY。两者方向完全不同。5. 常见问题排查与排障技巧这两个注解的参数倒是不复杂但真正到项目里出问题的时候总会冒出一些奇怪的状况。下面整理几个我见过的高频问题每个都会有排查思路。5.1 注解没生效序列化还是按字段原名走这种情况多半不是注解写错了而是类没有被 Jackson 识别。如果类上没有公开的 getter/setter字段是private而类上又没有JsonAutoDetect时Jackson 默认不会去碰这些字段。虽然你把JsonProperty标上去了但如果注解只标在 setter 上而 getter 没标序列化时仍然按 getter 推导的名字输出。排查步骤先看这个类的字段是不是private再看有没有 getter/setter最后确认注解是标在字段上还是方法上。我的建议是统一标在字段上这样无论序列化还是反序列化都走同一个规则不容易出现“输出对但接收不对”的割裂情况。还要检查项目里是不是有多个ObjectMapper实例。Spring Boot 默认的ObjectMapper是经过自动配置的但如果你在代码里自己new了一个ObjectMapper并且用它来序列化注解依然有效因为注解是 Jackson 内置处理的。这种情况问题多半出在类加载或方法可见性上而不是注解本身。5.2 反序列化时报 unknown property 错误这种错误通常和JsonProperty没关系而是 JSON 里出现了 Java 对象中不存在的字段。比如你定义了一个UserDTO只有name和age却收到了{name:张三,age:18,gender:男}。Jackson 默认遇到未知字段会报UnrecognizedPropertyException。解决办法有三个按项目情况选在类上加JsonIgnoreProperties(ignoreUnknown true)忽略当前类未知字段。在全局ObjectMapper上配置objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);在 Spring Boot 的 yaml 里统一关闭spring: jackson: deserialization: fail-on-unknown-properties: false有一点要说清楚如果 JSON 里的字段名和你期望的 Java 字段名对不上比如 JSON 里是user_nameJava 字段是userName这不属于 unknown property而是名字不匹配。你需要用JsonProperty(user_name)来映射或者用全局的PropertyNamingStrategy.SNAKE_CASE统一处理。5.3 加了JsonInclude(NON_EMPTY)但空字符串还在输出先说结论如果你确认自己用的是NON_EMPTY空字符串一定会被过滤掉。如果还在输出首先要查你是不是把注解放错了地方比如你想过滤 Map 内部的空 value却只对了 Map 字段本身设置NON_EMPTY而 Map 里的 value 仍然是那它当然还会出现。真正需要检查的几个点字段值是不是空字符串注意区分 这种空格字符串它不是。字段类型是不是自定义类型NON_EMPTY对自定义类型的判定没有那么乐观必要时需要自己给该类型实现isEmpty()逻辑。如果你用的是NON_NULL那空字符串本来就还会输出这不是 bug是你策略选错了。5.4 枚举类型的序列化控制枚举类型是JsonProperty在实战中很容易被忽略的使用场景。假设你有一个订单状态枚举public enum OrderStatus { JsonProperty(pending) PENDING, JsonProperty(paid) PAID, JsonProperty(cancelled) CANCELLED }不写JsonProperty时这个枚举序列化出来是PENDING、PAID这种大写字母名前端通常不喜欢这种风格。加上注解之后输出就变成了pending这种更容易符合接口约定的风格反序列化时前端传paid也能正确转成PAID。如果你需要更灵活的反序列化逻辑比如前端传1代表PAID、传2代表CANCELLED那就不能用JsonProperty了你要在枚举里写JsonCreator工厂方法。这里当个扩展知识点JsonProperty解决“名字对不上”JsonCreator解决“值要做转换”。5.5 长期维护项目中的命名策略默认值如果你的项目组统一用下划线风格定义接口字段并且在全局配置里设置了spring.jackson.property-naming-strategy: SNAKE_CASE那类里的userName会自动输出为user_name。这种全局命名策略在团队协作里很省心。但它也有副作用你如果某个字段必须要输出为特定名字比如uId而全局策略会让你变成u_id这时候唯一的拯救办法就是手动在字段上加JsonProperty(uId)。优先级规则刚才讲过显式注解永远高于全局命名策略。这种场景在大型系统中很常见因为一部分老接口已经定死了字段名另一部分新接口在按新规范走。需要灵活兼容时全局策略加上局部手动覆盖这一套组合是最稳定的。6. 结合 Spring Boot 的全局配置与性能影响最后补充一下和 Spring Boot 整合时容易忽视的配置细节以及这两个注解对序列化性能有没有影响。6.1 配置文件级别统一控制JsonInclude的全局配置在 Spring Boot 里有两种写法一种在 yaml 里spring: jackson: default-property-inclusion: non_null另一种是用Jackson2ObjectMapperBuilderCustomizer在代码里做定制Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder - { builder.serializationInclusion(JsonInclude.Include.NON_NULL); builder.failOnUnknownProperties(false); }; } }第二种方式更灵活因为它除了serializationInclusion以外还能配置很多其他 Jackson 属性适合团队里统一管理序列化行为。大型项目我更推荐这种方式因为所有 Jackson 的全局配置都有迹可循不用在 yaml 里翻来翻去地找。6.2 对性能的影响到底有多大很多人担心加了JsonInclude之后因为要做空值判断会影响序列化性能。实际上 Jackson 对JsonInclude的处理是在生成序列化器时就已经确定好的每个字段的过滤逻辑会被编译进对应序列化器里运行时的额外开销几乎可以忽略。相比之下真正影响性能的反而是重量级的自定义Serializer或者无脑地在 getter 里写复杂逻辑。JsonProperty和JsonInclude在框架内部都有成熟的优化路径放心用即可。真正要关注的是这样一件事过度使用NON_EMPTY会不会把本应该让前端感知的字段状态给吞了。如果部分字段是“有值就返、没值也要返个 null 占位”的语义那过滤掉 null 可能引发前端渲染异常。性能不用愁业务语义才是你选策略时要深思的。7. 我在实际项目中踩过的几个坑前几年做过一个对外提供查询服务的系统要求响应里所有字段都不能出现null。当时图省事在全局配置里直接设了default-property-inclusion: non_null结果前端反馈说某个“是否会员”的字段消失了因为他们判断字段名是否存在的代码风格是if (memberFlag in res)。字段被过滤之后memberFlag不存在他们走了 else 分支。原来是前端认为“字段存在但值为 null”和“字段不存在”是两种语义。从那次以后我就明白了过滤策略不是越激进越好要看消费端的判断习惯。还有一个和JsonProperty相关的教训。某次对接外部厂商的回调对方文档里写的是userID我们实体里写的是userId当时图快直接在 getter 上加了一个JsonProperty(userID)结果序列化没问题反序列化时因为 setter 上没有对应的注解又出了名字匹配不了的问题。后来统一改成把注解放在字段上一套逻辑两边生效再也没有这种“输出正常、接收报错”的割裂问题。把一个项目里所有接口的字段命名、空值策略统一梳理清楚比盲目堆注解有用得多。我现在的习惯是先和前端约定一份接口字段规范能覆盖 80% 的常规场景剩下的特殊场景才用JsonProperty打补丁。这样既不会注解满天飞也能在出现冲突时快速定位问题。希望这篇解析能让你少踩几个我当年踩过的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑