【踩坑经验】Jackson 当 NON_DEFAULT 遇到默认值为 true

5,097 阅读3分钟

背景

  • 熟悉 Jackson 的同学应该都知道,JackSon 可以设置 JsonInclude 来控制序列化,可以设置 ALWAYS,NON_NULL,NON_ABSENT,NON_EMPTY,NON_DEFAULT,CUSTOM,USE_DEFAULTS
  • 在当有复杂对象当默认值时,我们一般设置为 NON_DEFAULT 来避免序列化,减少反序列化时的内存消耗

复现

  • JsonInclude可以设置在类上,属性上,和全局上。一直以为设置的位置并不影响行为,只是写法不同罢了。全局设置最多是失效,但不会造成问题,但是最近工作中发现中并不是这般。请看如下例子
  • A,B,C 均为同一个对象,enable 默认为 true,在 Instance 中手工关闭,理论均应输出 {"enable":false}
public class JSONTest {

	public static void main(String[] args) throws JsonProcessingException {
		System.out.println(stringfy(A.INSTANCE));
		System.out.println(stringfy(B.INSTANCE));
		System.out.println(stringfy(C.INSTANCE));
	}
	
	private static String stringfy(Object o) throws JsonProcessingException {
			return new ObjectMapper().setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT)
					.writeValueAsString(o);
	}

	@JsonInclude(JsonInclude.Include.NON_DEFAULT)
	static class A {
		static A INSTANCE = new A().setEnable(false);
		boolean enable = true;
		public A setEnable(boolean enable) {
			this.enable = enable;
			return this;
		}
		public boolean isEnable() {
			return enable;
		}
	}

	static class B {
		static B INSTANCE = new B().setEnable(false);
		@JsonInclude(JsonInclude.Include.NON_DEFAULT)
		boolean enable = true;
		public B setEnable(boolean enable) {
			this.enable = enable;
			return this;
		}
		public boolean isEnable() {
			return enable;
		}
	}

	static class C {
		static C INSTANCE = new C().setEnable(false);
		boolean enable = true;
		public C setEnable(boolean enable) {
			this.enable = enable;
			return this;
		}
		public boolean isEnable() {
			return enable;
		}
	}
}
  • 但实际输出只有第一个是对的,其他两个均没输出,用户主动设置的属性就这么丢了,造成错误

原因

  • 那么这是 Bug 么?我们可以看到在 jackson 项目的 issue 里也有人有同样的问题 github.com/FasterXML/j…
  • 结论是,这是个 feature 并非 bug 。。。
  • 参阅文档,可知 NON_DEFAULT 在不同上下文中表现其实是不一样的。
    • 当在 POJO 的类上定义时,行为就是通常理解的 NON_DEFAULT
    • 而在其他地方时,行为则是不为空,或者不为基础类型的默认值,或者 Date 不为 0
  • 而上面的例子里,后两个因为 boolean 为基础类型,默认值为 false, 因此当前值等于默认值,就被 ignore 了。。。

解法

  • 那么既然不是 bug,肯定不会修,怎么办呢?两种解法
    1. 所有类均要加上注解
    2. 利用 Jackson 的注解支持传递的特效,因此可以定义一个接口或者基类,只要继承即可

根因

  • 既然都已经都发现了这个特殊的 feature,那么不妨继续看看其实怎么实现的?

首先,如何判断要不要输出呢?

  • com.fasterxml.jackson.databind.ser.BeanPropertyWriter#serializeAsField 中,我们可以看到 Jackson 在序列化字段时,会将当前值和_suppressableValue进行比较,如果一样即不输出。

如何获取 _suppressableValue ?

  • com.fasterxml.jackson.databind.ser.PropertyBuilder#buildWriter 中,我们可以看到如果 _useRealPropertyDefaults 为 true,则就用真实的对象 default,否则为 type 的 default

如何获得 _useRealPropertyDefaults ?

  • 查看构造方法,即可看到其关键部分,便在于此处的一个 merge 操作。将类上的include注解 和 config 里的进行 merge, merge 实际操作为如果两个一个为null/EMPTY就取另一个,如果两个有冲突,就以后一个为准。
  • 因为只有类上的NON_DEFAULT才是RealPropertyDefaults,因此只用判断 inclPerType 即可
  • 全局的a则是_defaultInclusion,props上的c则是在 buildWriter 时用propDef.findInclusion()获得。
  • 需要注意的是此处并没有判断属性上是否有 NON_DEFAULT,所以文档描述并不准确,当属性和类上同时定义了NON_DEFAULT,其行为也是不等于实际值,而不是不等于 type 的默认值