Contents
  1. 1. 要解决的问题
  2. 2. 解决过程
    1. 2.1. 1. 查看 maven 版本
    2. 2.2. 2. debug json 反序列化过程,找到原因
    3. 2.3. 3. 解决方案
    4. 2.4. 4. 根本原因
  3. 3. 结论

要解决的问题

希望在反序列化 json 到 bean 时,对于 json 中未出现的字段,在 bean 中赋上默认值。

例如
Person 类如下:

1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Person {
private String name;
private String address = "beijing"; // default value if json missing the age field
}

json:

1
{"name":"robert"}

反序列化后的 bean 为

1
Person(name="robert", address="beijing")

但实际上,发序列化的结果为

1
Person(name="robert", address=null)

解决过程

1. 查看 maven 版本

项目中 jackson 的配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Jackson dependency versions -->
<jackson.version>2.6.5</jackson.version>

配置升到最新后问题仍然存在。

2. debug json 反序列化过程,找到原因

json 反序列化是从

1
com.fasterxml.jackson.databind.ObjectMapper#_readMapAndClose

这个方法调用开始的,里面的一段代码为:

1
2
3
4
5
6
7
8
9
DeserializationConfig cfg = getDeserializationConfig();
DeserializationContext ctxt = createDeserializationContext(jp, cfg);
JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType);
if (cfg.useRootWrapping()) {
result = _unwrapAndDeserialize(jp, ctxt, cfg, valueType, deser);
} else {
result = deser.deserialize(jp, ctxt);
}
ctxt.checkUnresolvedObjectId();

在第 3 行找到的 JsonDeserializer 是 com.fasterxml.jackson.databind.deser.BeanDeserializer
从第 7 行代表进入 com.fasterxml.jackson.databind.deser.BeanDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)

函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// common case first
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken());
}
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
return deserializeFromObject(p, ctxt);
}
JsonToken t = p.getCurrentToken();
return _deserializeOther(p, ctxt, t);
}

vanillaDeserialize 为 false,最后走到了第 11 行,最后到了

1
com.fasterxml.jackson.databind.deser.BeanDeserializer#_deserializeUsingPropertyBased

然后到

1
com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator#build

在这个函数里有这样一段代码:

1
Object bean = _valueInstantiator.createFromObjectWith(ctxt, buffer.getParameters(_allProperties));

调用的是

1
com.fasterxml.jackson.databind.deser.ValueInstantiator#createFromObjectWith(com.fasterxml.jackson.databind.DeserializationContext, java.lang.Object[])

可以发现,createFromObjectWith 的第二个参数是数组,json 解出来的字段都放在了这个数组里。然后调用了 Person 类的全参构造函数,对于
缺失的字段自动补 null 值,这样就导致了 address 字段为 null。

3. 解决方案

去掉 @AllArgsConstructor 时,没有问题了,因为此时找到的 com.fasterxml.jackson.databind.deser.BeanDeserializer 的 vanillaDeserialize 字段为 true,会调用 vanillaDeserialize(p, ctxt, p.nextToken());,这个函数的实现非常明确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private final Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt, JsonToken t) throws IOException {
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
// [databind#631]: Assign current value, to be accessible by custom serializers
p.setCurrentValue(bean);
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.getCurrentName();
do {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) { // normal case
try {
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
continue;
}
handleUnknownVanilla(p, ctxt, bean, propName);
} while ((propName = p.nextFieldName()) != null);
}
return bean;
}

先用默认构造函数生成 bean,此时的 bean 是有默认值的,然后将 json 中出现的字段的值赋值给 bean,这样 address 就有值了。

4. 根本原因

看上去是声明了全参构造函数导致的,所以想尝试自己写全参构造函数,在 address 为 null 时给其赋默认值。
写完如下:

1
2
3
4
5
6
7
public Person(String name, String address){
this.name = name;
this.address = address;
if(this.address == null){
this.address = "beijing";
}
}

继续走刚才 debug 的流程,发现居然没有请求这个全参构造函数。

那问题就是 @AllArgsConstructor 生成的全参函数有不同之处,jackson 能够识别出来并用于反序列化。查看 jar 包中 Person 类的代码发现其全参构造函数如下:

1
2
3
4
5
@ConstructorProperties({"name", "address"})
public Person(String name, String address){
this.name = name;
this.address = address;
}

所以,区别就是 @ConstructorProperties({"name", "address"}) 这个注解,这个注解的作用是指定构造函数参数的名字,Spring 可根据参数的名字注入 bean。
但最终为什么这样注解了,jackson 就调用了全参构造函数还不得而知,猜测是 jackson 在 _findRootDeserializer 这一步时,是找最适合的构造函数。

可以通过设置 @AllArgsConstructor(suppressConstructorProperties=true) 来禁用 @ConstructorProperties.

结论

Lombok 的 @AllArgsConstructor 注解导致 Jackson 反序列化时调用了全参构造函数,将没有出现的字段都赋值为 null 了。

修改方式:

  1. 不使用 @AllArgsConstructor
  2. 使用 @AllArgsConstructor 但是不让其在全参构造函数上加入 ConstructorProperties 注解,声明方式改为 @AllArgsConstructor(suppressConstructorProperties = true)
Contents
  1. 1. 要解决的问题
  2. 2. 解决过程
    1. 2.1. 1. 查看 maven 版本
    2. 2.2. 2. debug json 反序列化过程,找到原因
    3. 2.3. 3. 解决方案
    4. 2.4. 4. 根本原因
  3. 3. 结论