Java JSON序列化忽略Null值:Jackson配置、踩坑与最佳实践
2026/9/18 16:46:04 网站建设 项目流程

1. Null值到底怎么就成了序列化路上的拦路虎

做Java后端的人,几乎每天都在跟Jackson打交道。很多人第一次遇到"忽略Null值"这个需求,都是被接口文档或前端同事逼出来的:明明是个空字段,返回给前端却是"field": null,数据量倒是小事,关键是前端拿到的对象模型里全是意义不明的null,各处都要判空。我自己最早是在对接第三方开放平台时被卡过一道:对方接口做了严格的字段校验,多传一个显式的null字段,直接返回参数非法。从那以后,我才认真研究起Jackson序列化JSON时忽略Null值的各种姿势。

1.1 一个让我下定决心处理Null值的真实场景

2022年做一个电商中台项目,订单详情接口要返回给小程序端和内部管理系统复用。一开始图省事,直接用实体类序列化返回,结果订单对象里有物流信息、发票信息、优惠明细等十几个可空字段,序列化出来的JSON长这样:

{ "orderId": "ORD20220011", "userId": 12890, "consignee": "张三", "phone": "138****1234", "address": "上海市浦东新区", "invoice": null, "logistics": null, "couponList": null, "remark": null }

小程序端拿到这串数据,每次渲染前都要判断invoice != null && invoice.xxx,代码里到处是空值守卫。更麻烦的是日志系统要全文检索JSON,大量null占着索引空间。后来前端组长直接找过来:"能不能别把null字段返回给我?" 我这才意识到,忽略Null值不是洁癖问题,而是接口设计规范里的一部分。

1.2 先搞清楚:忽略Null值有四个层次的语义

在动手配置之前,建议先把"忽略Null值"拆开看,因为它至少包含四个完全不同的语义层次:

  • 序列化时不输出null字段:这是最常见需求,即{"name":"x","age":null}只输出{"name":"x"}
  • 反序列化时忽略JSON中的null字段:假设对方传来的JSON里有{"name":null},反序列化时不覆盖Java对象现有的name值。
  • 忽略空字符串、空集合、空Optional:这是"空值"的外延,并非严格意义的null,但很多人混在一起处理。
  • 忽略值为null的集合元素或Map条目:比如Map里有个key对应的value是null,key要不要保留?

很多教程只讲第一层,导致项目里改了配置后,反序列化行为也跟着变,踩坑了才知道这几个层次需要单独控制。我在下文的每个章节里会把这几层分开讲,因为Jackson对它们的控制机制确实不一样。

2. 全局配置:ObjectMapper的setSerializationInclusion到底改了什么

先讲最直接、也最容易被搜索引擎推到前面的方案:在ObjectMapper上做全局配置。

ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

这段代码几乎出现在所有相关博客里,但很多人抄完并不知道它背后改了什么,更不知道它和setDefaultPropertyInclusion之间其实有细微差别。

2.1 四种写法等价但风格不同

我见到的全局配置写法至少有四种:

写法影响范围说明
mapper.setSerializationInclusion(NON_NULL)序列化只影响序列化,不碰反序列化
mapper.setDefaultPropertyInclusion(NON_NULL)序列化+反序列化同时影响序列化和反序列化的属性包含策略
mapper.setSerializationInclusion(NON_NULL); mapper.setDeserializationInclusion(NON_NULL);序列化+反序列化分开设置,语义最明确
spring.jackson.default-property-inclusion=non_null容器内全局生效Spring Boot配置文件写法,作用于自动装配的ObjectMapper

为什么同样一个NON_NULL,set和setDefault会有区别?因为Jackson的配置分两层:SerializationConfig(序列化配置)和DeserializationConfig(反序列化配置)。setSerializationInclusion只改SerializationConfig,而setDefaultPropertyInclusion改的是两个config共享的默认属性包含规则。如果只想控制序列化输出,用第一种最安全;如果希望反序列化时JSON里的null值也不要覆盖目标对象的已有字段,用第二种或第三种。

在我维护的项目里,我更倾向于分开设置。因为有的接口需要接收第三方传来的null值来置空某个字段,一旦全局把反序列化也设成NON_NULL,所有DTO里的字段都"失去"了被置空的能力。这个坑特别隐蔽,我在第6章还会再讲。

2.2 Include枚举的每个选项都适合什么场景

Jackson 2.x里,JsonInclude.Include提供了几个可选值,很多教程一笔带过,实际用起来差别很大:

  • ALWAYS:无论字段值是什么都输出,这是默认行为,等于什么都没配。
  • NON_NULL:非null才输出。最常用,但不处理空字符串。
  • NON_ABSENT:在NON_NULL基础上,额外处理Optional、AtomicReference等"引用包装类型"为空的情况。
  • NON_EMPTY:在NON_NULL基础上,连空字符串、空数组、空集合、空Map都不输出。注意,它不等同于"非空才输出",比如Optional.empty()也会被忽略。
  • NON_DEFAULT:字段值不等于默认值时才输出。比如int类型默认0,boolean默认false,List默认为null或空集合,这些"默认值"都不输出。
  • USE_DEFAULTS:不指定,沿用上一级配置。

我在做接口规范时,推荐一个基本原则:对外API的响应对象优先用NON_NULL,查询列表或详情时如果某些字段对前端有"有没有"的语义区别,再用NON_EMPTY。举个例子,一个退货单对象的refundReason字段,为null表示"未申请退款",为空字符串""可能表示"申请退款但没填原因",如果一刀切用NON_EMPTY,这两种语义就合并了,前端没法区分。

2.3 全局配置的副作用:别让整个项目裸奔

全局配置最大的问题就是"影响全局"这四个字。我见过一个兄弟项目,架构师在公共配置里加了NON_NULL,结果线上一个老接口原来返回{"score":null},前端根据score !== null判断"该用户未评分",配置上线后字段直接消失,前端取到undefined,判断逻辑瞬间失效,事后紧急回滚。

所以当你决定动全局ObjectMapper时,先想清楚三件事:

  • 现有接口有哪些字段依赖"返回null"来表达语义?
  • 内部服务之间的RPC JSON是否会把null字段当作一种状态传递?
  • 日志、监控、Mock数据生成器是否按固定JSON结构做校验?

要是无法确认,宁可先在DTO字段上用注解做局部控制,也别一上来就改全局。第3章的注解方案就是用来做"局部控制"的。

3. 局部精准控制:@JsonInclude的注解使用姿势

全局配置适合"整个项目约定一致"的场景,但现实里我们经常遇到"大部分接口忽略null,个别接口必须保留某个null字段"的需求。这时候@JsonInclude注解就是最趁手的工具。

3.1 注解的三个作用位置和优先级

@JsonInclude可以放在类、字段、getter方法三个位置:

// 类级别:整个User对象的null字段都不输出 @JsonInclude(JsonInclude.Include.NON_NULL) public class User { private String name; // 字段级别:这个字段特殊处理,null也要输出 @JsonInclude(JsonInclude.Include.ALWAYS) private String remark; // getter方法级别:同样可以单独配置 @JsonInclude(JsonInclude.Include.NON_NULL) public String getNickname() { return nickname; } }

优先级从高到低是:字段级别 > getter方法级别 > 类级别。Jackson在构建序列化器时,会先看字段上有没有注解,再看getter,最后落到类上。这意味着你可以在一个「类级别配了NON_NULL」的DTO里,单独用@JsonInclude(ALWAYS)把一个关键字段"抢救"回来。

3.2 组合用法:既要忽略null,又要保留关键字段

我做过一个比较典型的接口:查询用户订单列表。列表场景下,冗余字段越少越好,所以类级别配了NON_NULL;但有个extraInfo字段是预留给将来扩展的,即使现在是null,也要求接口输出"extraInfo":null,防止前端将来取不到字段直接崩溃。

@Data @JsonInclude(JsonInclude.Include.NON_NULL) public class OrderListVO { private String orderId; private BigDecimal amount; @JsonInclude(JsonInclude.Include.ALWAYS) private Map<String, Object> extraInfo; }

这样配置后,序列化结果是:

{ "orderId": "ORD20220011", "amount": 99.00, "extraInfo": null }

同一个对象,用同一套配置,想忽略的忽略,想保留的保留。这个组合用法比全局配置灵活得多,也是我在实际项目里用得最多的方案。

3.3 继承与多态场景下注解失效的坑

@JsonInclude注解有个让人意外的地方:它没有被@Inherited标记。也就是说,父类上的@JsonInclude注解不会自动继承给子类。假设你定义一个BaseResponse:

@JsonInclude(JsonInclude.Include.NON_NULL) public class BaseResponse { private String code; private String message; }

然后子类OrderResponse extends BaseResponse,并且子类里新加了字段,那么在序列化OrderResponse时,只有子类自己声明的字段会走父类的注解默认值吗?实际上不会。@JsonInclude作为一个类级别的注解,Jackson在初始化时读取的是运行时类上的注解。如果子类没有标注@JsonInclude,Jackson会退回到ObjectMapper的默认配置,而不会自动使用父类上的NON_NULL。

所以如果你有父类控制公共字段,子类增加业务字段的这种继承结构,请务必在每个子类上重新标注@JsonInclude,或者封装一个自定义注解,用@JacksonAnnotationsInside@JsonInclude组合进去:

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @JsonInclude(JsonInclude.Include.NON_NULL) public @interface ApiResponse { }

这样子类或者字段上直接标注@ApiResponse,语义更清晰,也不容易漏配。

4. 特殊类型的边界:Optional、空字符串、Map中的null

忽略Null值,最怕的就是"以为配了NON_NULL就万事大吉",结果被Optional.empty()、空字符串、Map里的null值狠狠教育了一顿。这些边界情况在真实项目里太常见了,单独开一章讲。

4.1 NON_ABSENT 到底是干嘛的

很多教程提到NON_ABSENT时都一笔带过,说它是"NON_NULL基础上扩展"。实际上它专门处理的是"引用包装类型"(referent types)——也就是OptionalAtomicReference这类容器。

先看个直观例子:

public class QueryResult { private String data; private Optional<String> extra; }

默认情况下,如果extraOptional.empty(),Jackson会把它序列化成"extra":null。此时配NON_NULL,这个字段会被忽略;配NON_ABSENT,同样会被忽略。看起来两者没什么区别。

真正的区别在于:Optional里包着一个null。比如:

Optional<String> extra = Optional.ofNullable(null);

这种情况下,Optional.ofNullable(null)实际上返回的是Optional.empty(),所以结果还是null。NON_NULL和NON_ABSENT表现一致。但是如果你自己实现了一个类似Optional的包装类,或者使用AtomicReference,NON_NULL只判断"外层引用是否为null",而NON_ABSENT会通过ReferenceType内部的逻辑,判断"被包装的值是否为空"。

对于绝大多数项目,直接用NON_NULL就够了;如果你的代码里有大量Optional<T>字段,并且希望Optional.empty()Optional.of(null)都不输出,NON_ABSENT更保险。不过我的建议是:DTO里尽量不要用Optional,它是Java 8留给流式处理和函数式编程用的,放在DTO里只会增加序列化和反序列化的心智负担。

4.2 空字符串不是null:NON_EMPTY帮你兜底

我曾经处理过一个很典型的"假空"问题。客户系统的地址字段允许传null,也允许传空字符串""。数据库里两种值都存在,但前端只认null表示"没有"。当接口配了NON_NULL后,空字符串照样会输出"address":"",前端依然要做额外的address.length > 0判断。

如果业务上确实不需要区分null和空字符串,可以直接把Include级别调整成NON_EMPTY。这时候空字符串、空数组、空集合、空Map都会被忽略:

@JsonInclude(JsonInclude.Include.NON_EMPTY) public class AddressDTO { private String city; // "" 会被忽略 private String detail; // null 会被忽略 private List<String> tags; // 空list 会被忽略 }

但要注意两个坑:

  • NON_EMPTY自定义类型的判断不是自动的。它依赖isEmpty()方法的识别,Jackson默认对Collection、Map、String、数组这些常见类型有内置判断,对自定义POJO,如果没有isEmpty()方法,它不会认为这个对象"空"。
  • Optional.empty()NON_EMPTY也会忽略,因为Optional内部可以判断"是否为空"。

所以,是否要从NON_NULL升级到NON_EMPTY,要去业务里核对一下"空字符串"是否有意义,不要为了少几个字符就盲目升级。

4.3 Map中的null值不会乖乖消失

我遇到的最隐蔽的问题,是Map字段的null值过滤。假设DTO里有一个Map<String, Object> attributes,里面某个key对应的value是null:

Map<String, Object> attributes = new HashMap<>(); attributes.put("size", "L"); attributes.put("color", null);

即使类上标注了@JsonInclude(NON_NULL),序列化后color这个key依然会出现在JSON里,值为null。为什么?因为NON_NULL的作用范围是"Bean属性",而不是"Map条目"——Jackson序列化Map时,是把每个key-value当作一个entry直接输出,并不会进入Bean属性的null值判断逻辑。

要过滤Map里的null value,有几种思路:

  • 在put之前,自己判断value为null就不放进去,最简单,但侵入业务代码。
  • 全局注册自定义MapSerializer,在序列化时drop掉null条目。这里给一个参考实现思路,用BeanSerializerModifier替换Map的序列化器,然后内部判断value == null就continue。这个属于进阶玩法,我在第5章详细讲。

这里先把结论记住:字段上的NON_NULL管不住Map里的value,列表里的null元素它同样管不住。如果第三方对JSON里显式null很敏感,这些边界得单独处理。

5. 进阶方案:为什么BeanSerializerModifier和FilterProvider要慎用

网上关于"Jackson忽略Null值"的教程,大部分止步于注解和全局配置。但到了真实项目里,你会遇到更变态的需求:某个接口要根据请求参数动态决定是否忽略null、某个Map字段也要过滤null条目、某个第三方SDK要求序列化时首字母不能输出null字段。这时候就得考虑自定义序列化方案了。

5.1 BeanSerializerModifier的一个常见误解

先辟个谣:很多博客推荐用BeanSerializerModifier来修改序列化器,说是可以"移除null属性"。实际上,BeanSerializerModifier是静态修改BeanSerializer结构的,它发生在序列化器构建阶段,根本感知不到运行时某个字段的值是不是null

我在自己的项目里试过这种写法,在modifySerializer里遍历beanProperties,想找到值为null的字段然后移除——结果是编译期没问题,运行时空指针满天飞。原因很简单:BeanPropertyWriter包装的是属性定义,不是属性值。你想在build阶段做"值过滤",方向就错了。

那么BeanSerializerModifier真正能做什么?它可以做结构性修改

  • 为某个类型替换一个全新的Serializer
  • 增加/删除/排序序列化属性
  • 给字段统一加前缀或后缀

所以,如果你的目标是把所有Map字段的null条目过滤掉,可以通过BeanSerializerModifier把MapSerializer替换成自定义版本。这个思路是对的,注意它替换的是整个类型的序列化器,而不是在Bean属性级别做过滤。

5.2 自定义PropertyFilter:运行时值过滤的通用解法

要实现在运行时判断字段值是否为null并决定是否输出,标准做法是实现JsonSerializer或者在FilterProvider里做文章。比较通用的是自定义PropertyFilter

public class NullAwareFilter extends SimpleBeanPropertyFilter { @Override public void serializeAsField(Object pojo, JsonGenerator gen, SerializerProvider prov, PropertyWriter writer) throws Exception { // 通过反射拿到字段当前值 Object value = writer.getMember().getValue(pojo); if (value != null) { writer.serializeAsField(pojo, gen, prov); } } }

然后在ObjectMapper上启用FilterProvider:

ObjectMapper mapper = new ObjectMapper(); mapper.setFilterProvider(new SimpleFilterProvider() .addFilter("nullAwareFilter", new NullAwareFilter()));

需要过滤的类上加注解:

@JsonFilter("nullAwareFilter") public class User { private String name; private String email; }

这种做法有一个很明显的缺点:通过反射拿值,性能比BeanSerializer原生的getter调用要差一些,而且如果字段是private且没有getter,writer.getMember().getValue()可能抛异常。所以我在生产环境中很少用这个方案来全局过滤null,更多是用在"动态决定某个字段是否忽略"的场景。

5.3 针对Map和List的null元素过滤方案

再来解决第4章埋下的坑:如何让Map里的null value、List里的null元素在序列化时被过滤掉。既然字段级别的NON_NULL管不住容器内部,那就只能customize容器本身的序列化器。

以Map为例,继承MapSerializer太重了,可以直接在一个自定义的JsonSerializer<Map>里做:

public class NullSkippingMapSerializer extends JsonSerializer<Map<?, ?>> { @Override public void serialize(Map<?, ?> value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeStartObject(); for (Map.Entry<?, ?> entry : value.entrySet()) { if (entry.getValue() != null) { gen.writeObjectField(String.valueOf(entry.getKey()), entry.getValue()); } } gen.writeEndObject(); } }

然后在SimpleModule里注册,专门用于某个字段类型:

SimpleModule module = new SimpleModule(); module.addSerializer(Map.class, new NullSkippingMapSerializer());

说实话,这个需求我用到的次数很少,因为绝大多数情况下,我在组装Map之前就做好了null过滤。但是一旦你用Map来承载动态字段(比如接口的extend字段),这个方案就是救命的。注意,如果你直接注册到Map.class,会导致项目里所有Map字段的序列化都走这个逻辑,需谨慎;更好的做法是定义一个Map.class的子类,或者用@JsonSerialize(using=...)只针对特定字段注册。

6. 实战踩坑:Spring Boot、Redis序列化下的null值问题

理论讲完,落到实际项目里,坑最多的不是怎么配置,而是配置完之后的连锁反应。这里分享三个我真实踩过的坑,都会让你对"忽略Null值"有更立体的认知。

6.1 Spring Boot中改ObjectMapper的正确姿势

在Spring Boot项目中,很多人的第一反应是自己在配置类里new一个ObjectMapper返回。我一开始也这么干过,结果发现Spring容器里的Jackson2ObjectMapperBuilderCustomizer自定义项、JavaTimeModuleParameterNamesModule全部失效,LocalDateTime反序列化直接报错。

正确做法有三种,按推荐程度排序:

// 方式一:配置文件(简单,覆盖大部分场景) spring.jackson.default-property-inclusion=non_null // 方式二:自定义Jackson2ObjectMapperBuilderCustomizer(推荐) @Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder -> { builder.serializationInclusion(JsonInclude.Include.NON_NULL); // 这里还可以配时间格式等其他规则 builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); }; } } // 方式三:完全用@Primary替换ObjectMapper(除非你确定自己在做什么,否则别用)

方式二比方式一更值得推荐的原因在于:它是在Spring Boot已有的ObjectMapper构建链路上追加配置,不会破坏自动配置好的模块。我在配置完成后,还会用一个单元测试来验证:

@Test void testNullIgnored() throws Exception { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); Map<String, Object> obj = new HashMap<>(); obj.put("a", null); obj.put("b", "value"); String json = mapper.writeValueAsString(obj); assertEquals("{\"b\":\"value\"}", json); }

别小看这个测试,很多人在Spring Boot里配了spring.jackson.default-property-inclusion=non_null,却发现在某些接口里依然输出null,大概率是因为那个接口手动构造了ObjectMapper,绕过了自动配置。

6.2 Redis序列化时null值消失引发的数据语义问题

另一个高频踩坑场景是Redis缓存。用Jackson2JsonRedisSerializer存对象时,如果实体类上配了NON_NULL,存入Redis的JSON里就没有null字段;读出来反序列化时,缺失字段会变成Java对象字段声明的默认值(null、0、false等)。看起来没毛病,但有两类问题:

一是无法区分"字段不存在"和"字段值为默认值"。比如一个优惠券对象,usedTime字段为null表示"未使用"。配置忽略null后,缓存里根本没有usedTime字段;哪天你改造缓存结构,反序列化时usedTime就是null,依然能表达"未使用"。但如果业务里有一些字段是故意不缓存、靠数据库兜底的,你从Redis读出来的对象可能"看起来完整",实际上关键字段全是null或0,走到事务层才发现。

二是反射和泛型推导会踩坑。部分框架在反序列化时通过Class.getDeclaredField("xxx")来校验字段是否存在,字段缺失直接抛异常;虽然Jackson本身不会这样,但上层封装框架不一定按Jackson的规则来。

我的建议是:Redis缓存对象和接口返回对象务必分开,不要共用同一个VO。缓存对象可以保留null字段,用NON_NULL只影响接口出参;或者干脆缓存不用JSON格式,用JDK序列化、Kryo等二进制方案,省掉JSON解析的CPU开销。很多同学为了少写两个类,把一个VO用在接口、缓存、MQ消息等所有场景,最后每改一次序列化配置就要惊呼一次"怎么这里也变了"。

6.3 反序列化时配置不对称导致的严重事故

最后这个坑,是第2章提到的"setSerializationInclusion和setDefaultPropertyInclusion"区别的实战化。

我维护的一个旧系统,从.NET那边迁移过来,对方传JSON里经常带显式null,比如{"userName":null,"age":18},本意是把库里旧的userName清空。由于全局用了setDefaultPropertyInclusion(NON_NULL),反序列化时Jackson看到JSON里的userName:null,直接跳过了这个属性,Java对象里原来的userName仍然保留旧值,数据库里那条记录的userName根本没被清空。这个bug排查了很久,因为日志里看到的是DTO对象被正确构建,但更新SQL那里用旧对象覆盖了userName。

解决办法很简单:把全局配置拆开:

mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 反序列化继续使用默认的ALWAYS,即JSON里的null照常覆盖Java字段

所以真的不要图省事一把梭。序列化和反序列化的"空值处理策略"是两个独立的维度,配置时务必要分开想清楚:接口出参要干净,入参却可能需要接收null来表达"清空/置空"语义。

7. 和Fastjson对比:忽略Null这种需求两个库的哲学差异

热搜词里有"jackson和fastjson哪个好",正好可以展开对比。虽然这个话题被聊烂了,但我发现很多人忽略了一个最基础的差异——这两个库对null字段的默认策略几乎是对着干的。

7.1 默认行为就是反的

行为JacksonFastjson
默认序列化null字段输出"field":null默认不输出null字段
需要输出null字段时默认配置下自动输出必须指定SerializerFeature.WriteMapNullValue
忽略null字段的配置JsonInclude.Include.NON_NULL默认就是忽略,无需配置
控制空字符串NON_EMPTY通过SerializerFeature.WriteNullStringAsEmpty控制

这个差异对"忽略Null值"这件事的影响很大。如果你从Fastjson切换到Jackson,或者反过来,很多代码看起来是"升个版本",实际null字段行为全变了,前端可能收到多一倍的null字段,或者之前能拿到的字段突然消失。

Fastjson默认不输出null字段,所以"忽略Null"这个需求在Fastjson世界里几乎是默认成立的;但在Jackson的世界里,你需要显式配置。我见过不少团队在迁移时踩了这个默认策略差异的坑,最后都是在Jackson里配一个全局NON_NULL,才模拟出Fastjson的默认行为。

7.2 为什么我最终选择继续用Jackson

抛开性能和生态不谈,单从"忽略Null值"这个需求的工程化角度看,Jackson的粒度控制明显更细,也更规范:

  • Jackson用@JsonInclude注解表达意图,代码即文档,看了就知道这个字段将来会不会出现在JSON里。
  • Fastjson常年在类上用@JSONField(serialzeFeatures = SerializerFeature.WriteMapNullValue)这类配置,可读性差一些,而且serialzeFeatures拼错了历史包袱至今还挂在API上。
  • 从Spring Boot 2.x开始,官方默认JSON库就是Jackson,意味着你引入spring-boot-starter-web,就不需要额外引入任何依赖;Fastjson虽然也有starter,但那属于第三方适配,维护节奏和Spring版本绑定没有那么及时。

当然,Fastjson在复杂JSONPath提取、大JSON文本解析等场景确实有优势,但在一个以Spring Boot为核心的团队里,为了一个"忽略Null值"需求去混用两套JSON库,完全不划算。我自己只在写脚本或做数据清洗时用Fastjson,工程代码里一律Jackson。

7.3 老项目里Fastjson过渡到Jackson的兼容技巧

如果你的老项目正在从Fastjson迁移到Jackson,并且希望最小化对前端的影响,我建议先做这一步:

@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer fastjsonCompatCustomizer() { return builder -> { // 模拟Fastjson默认不输出null字段的行为 builder.serializationInclusion(JsonInclude.Include.NON_NULL); // 如果老代码依赖Fastjson把空字符串序列化为"" // builder.serializerByType(String.class, new StringSerializer("")); }; } }

不过我的真实建议是:换就彻底换,别做太长时间的兼容层。两套JSON库混跑,最怕的就是同样的DTO在A接口用Jackson、B接口用Fastjson,前端拿到两种风格完全不同的JSON,排查问题时会怀疑人生。

8. 最后说几个用得顺手的小习惯

这个话题写到最后,分享几个我在实际项目中沉淀下来的习惯,算是个人的"顺手小抄"。

第一,DTO字段的null值策略尽量显式化。如果一个接口明确允许某个字段为null,要么在字段上加@JsonInclude(ALWAYS),要么在接口文档里重点标注。宁可多写几行注解,也别让人靠猜。

第二,启动时加一个ObjectMapper的自检用例。我每次改动Jackson配置,都会跑一个测试,专门验证三类场景:普通对象是否忽略null、Map里的null条目是否被处理、反序列化时null值是否按预期覆盖字段。这个测试成本极低,收益极高。

第三,不要试图在全局把所有边界都"安排得明明白白"。Jackson的配置体系本身是分层的,类注解覆盖全局配置、字段注解覆盖类注解、自定义序列化器兜底,你越是想在全局把所有情况都处理掉,后面就越容易被一个例外需求搞得焦头烂额。遇到特殊情况,局部打补丁,比全局改配置安全得多。

第四,团队约定优先于技术配置。如果你们的前后端对接是RESTful JSON,最好在接口规范里明确:"空值字段不返回,除非特殊说明"。有了这条约定,再去改代码,就不会被"这个null"、"那个空字符串"的琐碎细节反复纠缠。Jackson的NON_NULL只是这个约束的技术实现,真正的起点是团队一致认可的协议。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询