Jackson如何反序列化Java14中的record类型?

Java14近日发布,其中引入了新的record类型,虽然这是个预览特性,但是也不妨碍我们尝试下。

record类型

record类型和普通的类相比,有几个特点

  1. 每个字段都是private final的,类本身是final
  2. 类只有一个所有传递所有参数的构造函数
  3. 每个字段会自动会有一个get方法,但是方法名和字段名一致(没有get前缀
  4. 可以给字段添加注解,支持FIELD、METHOD和PARAMETER(如果一个注解是FIELD、METHOD和PARAMETER的,那么注解会在字段、get方法和构造函数参数上同时出现)
  5. toStringequalshashCode方法都是自带的

这几个特点决定了这个非常适合做DO(Model),DTO(比如RPC中定义的各种entity,http返回的json)。

由于业务中经常用到jackson来序列化反序列化对象,今天我们就来试一试jackson对record类型的支持。

如何使用jackson反序列化record对象

如果我们按照原来的方式来反序列化record(如果字段上没有@JsonProperty注解的话),会直接报错:
InvalidDefinitionException: Cannot construct instance of Person (no Creators, like default construct, exist)

这是因为jackson没法找到构造函数和字段的映射,所以我们自己指定默认的映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
// from https://gist.github.com/youribonnaffe/03176be516c0ed06828ccc7d6c1724ce
JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
@Override
public String findImplicitPropertyName(AnnotatedMember m) {
if (m.getDeclaringClass().isRecord()) {
if (m instanceof AnnotatedParameter parameter) {
return m.getDeclaringClass().getRecordComponents()[parameter.getIndex()].getName();
}
}
return super.findImplicitPropertyName(m);
}
};
objectMapper.setAnnotationIntrospector(implicitRecordAI);

可以看到,jdk提供了一系列方法来处理record类型,比如getRecordComponents来获取每个字段(包含了字段在构造函数中的位置)。

这样我们就能正常反序列化了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@JsonIgnoreProperties(ignoreUnknown = true)
public record Person(
Integer id,
String name
) {
public static void main(String[] args) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
@Override
public String findImplicitPropertyName(AnnotatedMember m) {
if (m.getDeclaringClass().isRecord()) {
if (m instanceof AnnotatedParameter parameter) {
return m.getDeclaringClass().getRecordComponents()[parameter.getIndex()].getName();
}
}
return super.findImplicitPropertyName(m);
}
};
objectMapper.setAnnotationIntrospector(implicitRecordAI);
String jsonStr = """
{
"id":1,
"name":"name1",
"alias": "alias1,alias2"
}
""";
Person person = objectMapper.readValue(jsonStr, Person.class);
System.out.println(person);
// Person[id=1, name=name1]
}
}

但是日常开发中,我们又会遇到不规范的json,比如Person对象有一个List<String> alias字段,但是数据源可能就是alias1,alias2这样的,所以我们得让record支持这样的玩法。

在使用record之前,我们是直接自定义JsonSetter来转换的:

1
2
3
4
5
6
private List<String> alias;

@JsonSetter("alias")
public void setAlias(String aliasStr) {
alias = Arrays.asList(aliasStr.split(","));
}

但是用了record后,由于alias是final的,没有set方法,这种做法就不太好处理了。

在官方的issue下找了下,发现可以使用JsonDeserialize注解,给某个具体的字段指定converter,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@JsonIgnoreProperties(ignoreUnknown = true)
public record Person(
Integer id,
String name,
// 如果字段有JsonDeserialize注解,那么一定要加上JsonProperty注解指定名字
@JsonProperty(value = "alias", access = JsonProperty.Access.WRITE_ONLY)
@JsonDeserialize(converter = AliasConverter.class)
List<String>alias
) {
public static void main(String[] args) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
// ......
};
objectMapper.setAnnotationIntrospector(implicitRecordAI);
String jsonStr = """
{
"id":1,
"name":"name1",
"alias": "alias1,alias2"
}
""";
Person person = objectMapper.readValue(jsonStr, Person.class);
System.out.println(person);
// Person[id=1, name=name1, alias=[alias1, alias2]]
}

/**
* 用于将逗号分割的String转成record中的List
*/
static class AliasConverter extends StdConverter<String, List<String>> {
@Override
public List<String> convert(String value) {
return Arrays.stream(value.split(",")).collect(Collectors.toList());
}
}
}

至此,反序列化上就没什么大坑了。

如何使用jackson序列化record对象

刚刚我们定制了alias字段的反序列化逻辑,现在我们要序列化,当然也要定制alias字段的序列化逻辑。

然后业务中经常遇到的场景是,序列化时忽略某些字段,比如我们增加了password字段,不要将这个字段序列化暴露出去。

摸索了下,可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@JsonIgnoreProperties(ignoreUnknown = true)
public record Person(
Integer id,
String name,
@JsonProperty(value = "alias", access = JsonProperty.Access.WRITE_ONLY)
@JsonDeserialize(converter = AliasConverter.class)
List<String>alias,
@JsonProperty(value = "password", access = JsonProperty.Access.WRITE_ONLY)
String password
) {
@JsonGetter("alias")
public String jsonGetAlias() {
return String.join(",", alias);
}

public static void main(String[] args) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
@Override
public String findImplicitPropertyName(AnnotatedMember m) {
if (m.getDeclaringClass().isRecord()) {
if (m instanceof AnnotatedParameter parameter) {
return m.getDeclaringClass().getRecordComponents()[parameter.getIndex()].getName();
}
// 反序列化时是检查member,所以加特殊处理
if (m instanceof AnnotatedMember member) {
for (RecordComponent recordComponent : m.getDeclaringClass().getRecordComponents()) {
if (recordComponent.getName().equals(member.getName())) {
return member.getName();
}
}
}
}
return super.findImplicitPropertyName(m);
}
};
objectMapper.setAnnotationIntrospector(implicitRecordAI);
String jsonStr = """
{
"id":1,
"name":"name1",
"alias": "alias1,alias2",
"password": "password1"
}
""";
Person person = objectMapper.readValue(jsonStr, Person.class);
System.out.println(person);
// person[id=1, name=name1, alias=[alias1, alias2], password=password1]
String jsonOut = objectMapper.writeValueAsString(person);
System.out.println(jsonOut);
// {"alias":"alias1,alias2","id":1,"name":"name1"}
}

/**
* 将String方式的alias转成List<String>
*/
static class AliasConverter extends StdConverter<String, List<String>> {
@Override
public List<String> convert(String value) {
return Arrays.stream(value.split(",")).collect(Collectors.toList());
}
}
}

这里面有几个点要注意:

  1. access = JsonProperty.Access.WRITE_ONLY 是指反序列时会set这个值,序列化时不会输出出去,这也保证了password不会被输出。
  2. 反序列化时是检查member,所以必须加m instanceof AnnotatedMember member
  3. jsonGetAlias方法带上了@JsonGetter("alias"),alias的序列化就会走这个方法

最终的示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 最终的示例代码
@JsonIgnoreProperties(ignoreUnknown = true)
public record Person(
Integer id,
String name,
// if this field have custom converter, JsonProperty is required
@JsonProperty(value = "alias", access = JsonProperty.Access.WRITE_ONLY)
@JsonDeserialize(converter = AliasConverter.class)
List<String>alias,
@JsonProperty(value = "password", access = JsonProperty.Access.WRITE_ONLY)
String password
) {
@JsonGetter("alias")
public String jsonGetAlias() {
return String.join(",", alias);
}

public static void main(String[] args) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
// get default propertyName from record
// thanks youribonnaffe
JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
@Override
public String findImplicitPropertyName(AnnotatedMember m) {
if (m.getDeclaringClass().isRecord()) {
if (m instanceof AnnotatedParameter parameter) {
return m.getDeclaringClass().getRecordComponents()[parameter.getIndex()].getName();
}
if (m instanceof AnnotatedMember member) {
for (RecordComponent recordComponent : m.getDeclaringClass().getRecordComponents()) {
if (recordComponent.getName().equals(member.getName())) {
return member.getName();
}
}
}
}
return super.findImplicitPropertyName(m);
}
};
objectMapper.setAnnotationIntrospector(implicitRecordAI);
String jsonStr = """
{
"id":1,
"name":"name1",
"alias": "alias1,alias2",
"password": "password1"
}
""";
Person person = objectMapper.readValue(jsonStr, Person.class);
System.out.println(person);
// Person[id=1, name=name1, alias=[alias1, alias2], password=password1]
String jsonOut = objectMapper.writeValueAsString(person);
System.out.println(jsonOut);
// {"alias":"alias1,alias2","id":1,"name":"name1"}
}

/**
* convert String to List, just for field alias
* <p>
* this is class-bound logic, so keep it internal
*/
static class AliasConverter extends StdConverter<String, List<String>> {
@Override
public List<String> convert(String value) {
return Arrays.stream(value.split(",")).collect(Collectors.toList());
}
}
}

Jackson如何反序列化Java14中的record类型?

https://robberphex.com/how-jackson-deserialize-a-record-in-java14/

作者

Robert Lu

发布于

2020-03-22

许可协议

评论