封面来源:碧蓝航线 箱庭疗法 活动CG

本文涉及的代码:mofan212/jackson-study

Jackson 源码仓库:

1. Jackson 概述

1.1 什么是 Jackson

Jackson 是一个用来处理 JSON 格式数据的 Java 类库,一个用来序列化和反序列化 JSON 的 Java 开源框架。从 GitHub 中的统计来看,Jackson 是最流行的 JSON 解析器之一。SpringMVC 的默认 JSON 解析器就是 Jackson。

GitHub 仓库对 Jackson 的介绍:

What-is-Jackson

Jackson 作为 Java 中用来处理 JSON 数据的类库而熟知,但它并不局限于 JSON。

1.2 Jackson 核心模块

Jackson 的核心模块是扩展(模块)所基于的基础。从 Jackson 2.x 开始,核心模块主要由三部分组成:

  1. jackson-core 核心包,提供基于“流模式”解析的相关 API,它包括 JsonPaser 和 JsonGenerator。 Jackson 内部实现正是通过高性能的流模式 API 的 JsonGenerator 和 JsonParser 来生成和解析 JSON。
  2. jackson-annotations 注解包,提供标准注解功能。
  3. jackson-databind 数据绑定包, 提供基于“对象绑定”解析的相关 API(ObjectMapper)和“树模型”解析的相关 API(JsonNode)。基于“对象绑定”解析的 API 和“树模型”解析的 API 依赖基于“流模式”解析的 API。

关于核心模块,GitHub 上是这么说的:

Jackson-Core-modules

jackson-databind 依赖 jackson-corejackson-annotations,添加 jackson-databind 之后, jackson-corejackson-annotations 也随之添加到项目工程中了。

因此,在 Maven 工程中导入依赖时,导入 jackson-databind 即可。

2. 快速开始

2.1 ObjectMapper 的使用

创建 Maven 项目,引入 Jackson 的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<jackson.version>2.15.2</jackson.version>
</properties>

<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>

编写一个简单的 Java 实体类:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author mofan 2020/12/14
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Entity {
private String name;
private int age;
private double height;
}

使用 Jackson 对实体进行序列化,或者反序列化成实体,需要实体中有无参构造方法、Getter 和 Setter 方法。Getter 方法用于序列化,Setter 方法用于反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
@SneakyThrows
public void testObjectMapper() {
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
User user = new User("mofan", 18, 177);
// 实体类解析成 JSON 数据
String string = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
String expectJson = """
{
"name" : "mofan",
"age" : 18,
"height" : 177.0
}
""";
JsonAssertions.assertThatJson(string).isEqualTo(expectJson);
// 从字符串中读取 JSON 数据
User readValue = mapper.readValue(string, User.class);
assertThat(readValue).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly("mofan", 18, 177.0);
}

writerWithDefaultPrettyPrinter() 方法的作用是 格式化 输出的 JSON 字符串。如果想要全局启用,可以在 ObjectMapper 类上进行设置,如:

1
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

2.2 JSON 来源

使用 ObjectMapper 对象对 JSON 进行序列化和反序列时,JSON 来源有很多种,readValue() 重载的方法参数有以下七种:

1
String | File | URL | InputStream | byte[] | DataInput | JsonParser

2.3 简单 JSON 串的读取

简单的反序列化

存在名为 /json/user.json 的文件,其内容为:

1
2
3
4
5
{
"name" : "mofan",
"age" : 18,
"height" : 178.1
}

尝试将不同来源的 JSON 信息序列化为 Java 对象:

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
@Test
@SneakyThrows
public void testDeserialization_1() {
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
URL url = getClass().getClassLoader().getResource("./json/user.json");
File file = new File(url.getFile());
// 从 File 中读取对象
User user1 = mapper.readValue(file, User.class);
assertThat(user1).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly("mofan", 18, 178.1);

String userJson = """
{
"name": "默烦",
"age": 20,
"height": 177.5
}
""";
// 从 String 中读取对象
User user2 = mapper.readValue(userJson, User.class);
assertThat(user2).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly("默烦", 20, 177.5);
// 从 Reader 中读取对象
User user3 = mapper.readValue(new StringReader(userJson), User.class);
assertThat(user3).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly("默烦", 20, 177.5);
// 从 InputStream 中读取对象
User user4 = mapper.readValue(new FileInputStream(file), User.class);
assertThat(user4).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly("mofan", 18, 178.1);
// 从字节数组中读取对象
User user5 = mapper.readValue(userJson.getBytes(StandardCharsets.UTF_8), User.class);
assertThat(user5).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly("默烦", 20, 177.5);
}

解析成对象数组、列表、映射

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
@Test
@SneakyThrows
public void testDeserialization_2() {
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
String userJson = """
{
"name": "默烦",
"age": 20,
"height": 177.5
}
""";
String jsonArray = """
[
{
"name": "mofan"
},
{
"name": "默烦"
}
]
""";

// 从 JSON 数组字符串中读取对象数组
User[] users1 = mapper.readValue(jsonArray, User[].class);
assertThat(users1).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly(tuple("mofan", 0, 0.0), tuple("默烦", 0, 0.0));

// 从 JSON 数组字符串中读取对象列表
List<User> userList = mapper.readValue(jsonArray, new TypeReference<>() {
});
assertThat(userList).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly(tuple("mofan", 0, 0.0), tuple("默烦", 0, 0.0));

// 从 JSON 数组字符串中读取对象列表
Map<String, Object> map = mapper.readValue(userJson, new TypeReference<>() {
});
assertThat(map).containsAllEntriesOf(Map.of("age", 20, "name", "默烦", "height", 177.5));
}

2.4 树模型

前面介绍了简单 JSON 串的读取,如果一个 JSON 串比较复杂,内部还有数组,或者嵌套了其他 JSON 串,这个时候使用前面的方式就无法解析了。为了解决这种复杂情况(开发中经常经常出现的情况),Jackson 引入了树模型的概念,涉及的类有 JsonNodeObjectNode 等等。

解析成 JsonNode

将 JSON 字符串解析成 JsonNode 有两种方式,可以使用 readValue() 指定第二个参数为 JsonNode.class,也可以使用 readTree() 方法,这个方法总是会返回一个 JsonNode 对象。

ObjectMapper 类中 readTree() 方法有多种重载,能将多种格式的 JSON 数据解析成 JsonNode:

readTree的各种重载

示例代码:

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
@Test
@SneakyThrows
public void testParseJSONIntoJsonNode() {
String userJson = """
{
"name": "默烦",
"age": 20,
"height": 177.5
}
""";
ObjectMapper mapper = new ObjectMapper();

JsonNode jsonNode1 = mapper.readValue(userJson, JsonNode.class);
assertThat(jsonNode1.get("name").asText()).isEqualTo("默烦");

JsonNode jsonNode2 = mapper.readTree(userJson);
assertThat(jsonNode2.get("age").asInt()).isEqualTo(20);

ObjectNode objectNode = ((ObjectNode) mapper.readTree(userJson));
objectNode.withObject("/other").put("type", "Student");
String string = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectNode);
String expectJson = """
{
"name" : "默烦",
"age" : 20,
"height" : 177.5,
"other" : {
"type" : "Student"
}
}
""";
JsonAssertions.assertThatJson(string).isEqualTo(expectJson);
}

JsonNode 的基本操作

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
@Test
@SneakyThrows
public void testJsonNodeOperation() {
String userJson = """
{
"name": "mofan",
"age": 20,
"hobby": [
"music",
"game",
"study"
],
"other": {
"type": "Student"
}
}
""";

ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(userJson);

// 获取 name 的字段值
String name = jsonNode.get("name").asText();
assertThat(name).isEqualTo("mofan");

// 获取 age 的字段值
assertThat(jsonNode.get("age").asInt()).isEqualTo(20);

// 获取 hobby 数组中第一个值
String firstHobby = jsonNode.get("hobby").get(0).asText();
assertThat(firstHobby).isEqualTo("music");

// 获取嵌套的 JSON 串中 type 的值
String type = jsonNode.get("other").get("type").asText();
assertThat(type).isEqualTo("Student");
}

添加与删除

使用树模型,甚至还可以向原 JSON 串中添加或删除数据,只不过这个时候不能用 JsonNode 来操作了,得使用它的子类 ObjectNode,比如:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@Test
@SneakyThrows
public void testAddDataToJSON() {
String userJson = """
{
"name": "默烦",
"age": 20,
"height": 177.5
}
""";
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

ObjectNode objectNode = mapper.readValue(userJson, ObjectNode.class);
// 添加普通字段
objectNode.put("weight", 65);
// 再嵌套一个 JSON
objectNode.withObject("/other").put("type", "Student");
// 添加一个数组
objectNode.withArray("hobby").add("music").add("game").add("study");
// 向数组指定位置添加一个值
objectNode.withArray("hobby").insert(1, "write");
// 向数组中嵌套一段 JSON
objectNode.withArray("jsonArray").add(mapper.readValue(userJson, ObjectNode.class));
// 向数组中添加一个 Object 节点
objectNode.withArray("objectArray").addObject().putObject("Test").put("test", "test");
String expectJson = """
{
"name" : "默烦",
"age" : 20,
"height" : 177.5,
"weight" : 65,
"other" : {
"type" : "Student"
},
"hobby" : [ "music", "write", "game", "study" ],
"jsonArray" : [ {
"name" : "默烦",
"age" : 20,
"height" : 177.5
} ],
"objectArray" : [ {
"Test" : {
"test" : "test"
}
} ]
}
""";
JsonAssertions.assertThatJson(mapper.writeValueAsString(objectNode)).isEqualTo(expectJson);

assertThat(objectNode.get("objectArray").get(0).isObject()).isTrue();
assertThat(objectNode.get("weight").asInt()).isEqualTo(65);
assertThat(objectNode.get("other").isObject()).isTrue();
assertThat(objectNode.get("other").get("type").asText()).isEqualTo("Student");
assertThat(objectNode.get("hobby").get(0).asText()).isEqualTo("music");

// 删除普通字段
objectNode.without("age");
// 删除嵌套 JSON 中的字段
objectNode.withObject("/other").without("type");
// 删除嵌套的 JSON,无论内部是否有字段
objectNode.without("other");
// 删除数组中某个值
objectNode.withArray("hobby").remove(0);
objectNode.withArray("jsonArray").removeAll();

// 向数组中添加一个 Object
User user = new User("mofan", 19, 177.5);
objectNode.withArray("addPojo").addPOJO(user);
// 向数组中添加带字段的 Object
objectNode.withArray("addPojoWithField").addObject().putPOJO("user", user);
expectJson = """
{
"name" : "默烦",
"height" : 177.5,
"weight" : 65,
"hobby" : [ "write", "game", "study" ],
"jsonArray" : [ ],
"objectArray" : [ {
"Test" : {
"test" : "test"
}
} ],
"addPojo" : [ {
"name" : "mofan",
"age" : 19,
"height" : 177.5
} ],
"addPojoWithField" : [ {
"user" : {
"name" : "mofan",
"age" : 19,
"height" : 177.5
}
} ]
}
""";
JsonAssertions.assertThatJson(mapper.writeValueAsString(objectNode)).isEqualTo(expectJson);
}

有关 ArrayNode 的方法还有很多,查看源码可以发现,其底层就是 List<JsonNode>,所以使用方式也和动态数组差不多。

比如,可以使用 set() 方法修改某个索引位置的数据,还可以使用 insert() 方法来给某个索引位置添加数据。

JsonNode 与 Object 的转换

JsonNode 与 Object 之间是可以进行转换的:

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
@Test
@SneakyThrows
public void testTransform() {
String userJson = """
{
"name": "默烦",
"age": 20,
"height": 177.5
}
""";
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

// JsonNode 转 Object
User user = mapper.readValue(userJson, User.class);
assertThat(user).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly("默烦", 20, 177.5);
// 或者
JsonNode jsonNode1 = mapper.readTree(userJson);
User user1 = mapper.treeToValue(jsonNode1, User.class);
assertThat(user1).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly("默烦", 20, 177.5);

// Object 转 JsonNode
User user2 = new User("mofan", 18, 178.1);
JsonNode jsonNode2 = mapper.valueToTree(user2);
String expectJson = """
{
"name" : "mofan",
"age" : 18,
"height" : 178.1
}
""";
JsonAssertions.assertThatJson(mapper.writeValueAsString(jsonNode2)).isEqualTo(expectJson);
}

2.5 转换成 JSON 串

简单的序列化

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
@Test
@SneakyThrows
public void testSerialization_1() {
User user = new User("默烦", 19, 178.2);
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

// 写为字符串
String str = mapper.writeValueAsString(user);
String expectJson = """
{
"name" : "默烦",
"age" : 19,
"height" : 178.2
}
""";
JsonAssertions.assertThatJson(str).isEqualTo(expectJson);

// 写为文件
File file = new File("test.json");
mapper.writeValue(file, user);
assertThat(file).exists();

// 写为字节流并读取
byte[] bytes = mapper.writeValueAsBytes(user);
assertThat(mapper.readValue(bytes, User.class))
.extracting(User::getName, User::getAge, User::getHeight)
.containsExactly("默烦", 19, 178.2);
}

在当前 Module 目录下,可以看到一个生成的名为 test.json 的文件,文件内容为:

1
2
3
4
5
{
"name" : "默烦",
"age" : 19,
"height" : 178.2
}

映射转换成 JSON

除了可以将实体类序列化成 JSON 字符串外,还可以将 Map 映射转换成 JSON 字符串,JSON 字符串是一种键值关系,而 Map 也是一种键值关系,因此能够相互转换也是情理之中。

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
@Test
@SneakyThrows
public void testSerialization_2() {
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

Map<String, Object> map = new HashMap<>(8);
map.put("name", "mofan");
map.put("age", 18);
map.put("hobby", new String[]{"code", "game", "study"});
List<String> list = new ArrayList<>();
list.add("book_1");
list.add("book_2");
list.add("book_3");
map.put("book", list);

String str = mapper.writeValueAsString(map);
String expectJson = """
{
"name" : "mofan",
"age" : 18,
"hobby" : [ "code", "game", "study" ],
"book" : [ "book_1", "book_2", "book_3" ]
}
""";
JsonAssertions.assertThatJson(str).isEqualTo(expectJson);
}

3. 流解析器

3.1 流解析器的概述

在 GitHub 上,关于 Jackson 的流解析器:

Streaming-parser

翻译一下就是:

使用数据绑定(实体与 JSON 之间的转换)很方便,树模型也很灵活,但是还有一种更规范的处理模型:增量(流)模型。 它是数据绑定和树模型的基础处理模型,它暴露给那些希望获得最高性能、控制解析或生成细节的用户。

3.2 基本使用

流解析器在使用时就像编写 XML 标签或者 HTML 标签一样,开头一个标识,结尾一个标识,中间书写内容,虽然书写起来很繁琐,但是操作 JSON 的性能和细节都能被编码者所掌握。

先来个 Quick Start:

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
@Test
@SneakyThrows
public void testStreamingParser() {
ObjectMapper mapper = new ObjectMapper();
JsonFactory factory = mapper.getFactory();

File file = new File("simpleJSON.json");
JsonGenerator generator = factory.createGenerator(file, JsonEncoding.UTF8);

generator.writeStartObject();
generator.writeStringField("msg", "Hello Jackson");
generator.writeEndObject();
generator.close();

JsonParser parser = factory.createParser(file);
JsonToken jsonToken = parser.nextToken();

jsonToken = parser.nextToken();
assertThat(parser.getCurrentName()).isEqualTo("msg");
assertThat(jsonToken).isEqualTo(JsonToken.FIELD_NAME);

jsonToken = parser.nextToken();
assertThat(jsonToken).isEqualTo(JsonToken.VALUE_STRING);

String msg = parser.getText();
assertThat(msg).isEqualTo("Hello Jackson");
parser.close();
}

同时也能在当前 Module 下看到生成的名为 simpleJSON.json 的文件,其内容为:

1
{"msg":"Hello Jackson"}

3.3 写入 JSON 数据

如果需要写入 JSON 数据,那么需要使用到 JsonGenerator 类,它是定义公共 API 编写的 JSON 内容的基类。也就是说,要写入 JSON 数据,就需要创建一个 JsonGenerator 对象,然后使用这个对象来写入数据。

要创建 JsonGenerator 实例,需要使用 JsonFactory 实例的 createGenerator() 工厂方法来创建实例。这个方法有很多重载,各种重载指定了数据写入的位置和写入的方式:

createGenerator方法的各种重载

成功得到 JsonGenerator 实例后,使用实例的各种 writexxxxx() 方法就可以写入数据了。

数据写入完成后,记得调用 close() 方法。当然,更推荐使用 try-with-resources 自动关闭资源。

来写入一些比 Quick Start 案例中要复杂一些的 JSON 数据:

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
@Test
@Order(1)
@SneakyThrows
public void testJsonGenerator() {
ObjectMapper mapper = new ObjectMapper();
JsonFactory factory = mapper.getFactory();

File file = new File("testJsonGenerator.json");

try (JsonGenerator generator = factory.createGenerator(file, JsonEncoding.UTF8)) {
// start {
generator.writeStartObject();
// "name" : "默烦"
generator.writeStringField("name", "默烦");
// "age" : 18
generator.writeNumberField("age", 18);
// "isGirl" : false
generator.writeBooleanField("isGirl", false);
// "hobby" : ["code", "study"]
generator.writeFieldName("hobby");
// [
generator.writeStartArray();
// code, study
generator.writeString("code");
generator.writeString("study");
// ]
generator.writeEndArray();
// end }
generator.writeEndObject();
}

// 数据写入结束,来读取一下
Map<String, Object> map = mapper.readValue(file, new TypeReference<>() {
});
assertThat(map).containsAllEntriesOf(Map.of(
"age", 18,
"hobby", List.of("code", "study"),
"isGirl", false,
"name", "默烦"));
}

在当前 Module 下,还会生成名为 testJsonGenerator.json 的文件,格式化后其内容为:

1
2
3
4
5
6
7
8
9
{
"name": "默烦",
"age": 18,
"isGirl": false,
"hobby": [
"code",
"study"
]
}

小总结

总的来说,写入数据并不难,编写代码时类似于写 XML 或 HTML 标签,有开始标志,有结束标志,中间写内容,最后记得关闭资源。

3.4 读取 JSON 数据

参考链接:介绍Jackson JsonParser解析json | 解析Java的Jackson库中Streaming API的使用

读取 JSON 数据时,需要使用到 JsonParser 类,它是定义公共 API 用于读取的 JSON 内容的基类。这个类的实例的创建方式和 JsonGenerator 类的实例的创建方式一样,都需要使用 JsonFactory 实例的 createParser() 工厂方法来创建。这个方法也有很多重载,实现了解析不同来源的 JOSN 数据:

createParser方法的各种重载

创建完 JsonParser 实例后,可以用其解析 JOSN 数据。JsonParser 工作方式是将 JSON 分解成一系列标记(token),逐个迭代遍历标记并进行解析。

试试通过循环所有标记并输出,JSON 数据用前一步生成的 testJsonGenerator.json 文件:

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
@Test
@Order(2)
@SneakyThrows
public void testGetToken() {
JsonFactory jsonFactory = new JsonFactory();
// 先执行 testJsonGenerator() 测试方法,确保存在 testJsonGenerator.json 文件
File file = new File("testJsonGenerator.json");
JsonParser parser = jsonFactory.createParser(file);

List<JsonToken> list = new ArrayList<>();
while (!parser.isClosed()) {
list.add(parser.nextToken());
}
assertThat(list).containsExactly(
JsonToken.START_OBJECT,
JsonToken.FIELD_NAME,
JsonToken.VALUE_STRING,
JsonToken.FIELD_NAME,
JsonToken.VALUE_NUMBER_INT,
JsonToken.FIELD_NAME,
JsonToken.VALUE_FALSE,
JsonToken.FIELD_NAME,
JsonToken.START_ARRAY,
JsonToken.VALUE_STRING,
JsonToken.VALUE_STRING,
JsonToken.END_ARRAY,
JsonToken.END_OBJECT,
null
);
}

对比 list 与原 JSON 信息:

Token标记与JSON数据的对比

这样一对比后,那不是直接清楚明了了?😎

既然如此,来试试读取数据,依旧是读取 testJsonGenerator.json 文件中的数据:

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
@Test
@Order(3)
@SneakyThrows
public void testJsonParser() {
ObjectMapper mapper = new ObjectMapper();
JsonFactory factory = mapper.getFactory();

File file = new File("testJsonGenerator.json");
JsonParser parser = factory.createParser(file);
while (!parser.isClosed()) {
if (JsonToken.FIELD_NAME.equals(parser.nextToken())) {
String fieldName = parser.getCurrentName();

// 移动到下一标记处
parser.nextToken();
if ("name".equals(fieldName)) {
assertThat(parser.getValueAsString()).isEqualTo("默烦");
}
if ("age".equals(fieldName)) {
assertThat(parser.getValueAsInt()).isEqualTo(18);
}
if ("isGirl".equals(fieldName)) {
assertThat(parser.getValueAsBoolean()).isFalse();
}
if ("hobby".equals(fieldName)) {
List<String> list = new ArrayList<>();
while (parser.nextToken() != JsonToken.END_ARRAY) {
list.add(parser.getValueAsString());
}
assertThat(list).containsExactly("code", "study");
}
}
}
}

如果标记指针指向的是字段,JsonParser 的 getCurrentName() 方法返回当前字段名称。

JsonParser 的 getValueAsString() 返回当前标记值的字符串类型,同理 getValueAsInt() 返回整型值,同样还有 Boolean、Double、Long 三种类型可以选择。

4. Jackson 的配置

4.1 配置前言

Jackson 有两种入门级配置机制,分别是:Features 和 Annotations,前者可以翻译成特征,比如在最开始使用的 enable(SerializationFeature.INDENT_OUTPUT) 就属于 Features,后者是注解的意思,可以在某些地方添加注解,达到想要的目标。

注意: Jackson 遵循 configure-first-then-use,即“先配置再使用”,也就是说在进行了 首次 序列化或反序列化操作后,再对 Jackson 进行的配置都不会生效,尽管这些配置在运行过程中并不会抛出异常。

4.2 常用的 Features

在 Jackson 的 GitHub 上,关于 Commonly used Features 有这些内容:

高级别的数据绑定配置

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
// to enable standard indentation ("pretty-printing"):
// 翻译: 格式化输出 JSON
mapper.enable(SerializationFeature.INDENT_OUTPUT);
// to allow serialization of "empty" POJOs (no properties to serialize)
// (without this setting, an exception is thrown in those cases)
// 翻译: 允许序列化为空的 POJO,否则会抛出异常
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// to write java.util.Date, Calendar as number (timestamp):
// 翻译: 把 java.util.Date, Calendar 输出为数字(时间戳)
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

// DeserializationFeature for changing how JSON is read as POJOs:

// to prevent exception when encountering unknown property:
// 翻译: 在反序列化时,忽略在 JSON 中存在但在 Java 对象中不存在的属性
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// to allow coercion of JSON empty String ("") to null Object value:
// 翻译: 强制JSON 空字符串("")转换为 null 对象值:
mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
// 在序列化时日期格式默认为 yyyy-MM-dd'T'HH:mm:ss.SSSZ
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,false)
// 在序列化时忽略值为 null 的属性
mapper.setSerializationInclusion(Include.NON_NULL);
// 忽略值为默认值的属性
mapper.setDefaultPropertyInclusion(Include.NON_DEFAULT);
// 将 null 值反序列化为基本类型时,不抛出异常
mapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
// 是否允许将枚举序列化或反序列化为数字,下述配置是允许
mapper.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, false);

低级别的 JSON 解析、生成细节配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// to allow C/C++ style comments in JSON (non-standard, disabled by default)
// (note: with Jackson 2.5, there is also `mapper.enable(feature)` / `mapper.disable(feature)`)
// 翻译: 在JSON中允许C/C++ 样式的注释(非标准,默认禁用)
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
// to allow (non-standard) unquoted field names in JSON:
// 翻译 : 允许没有引号的字段名(非标准)
mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
// to allow use of apostrophes (single quotes), non standard
// 翻译: 允许单引号(非标准)
mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);

// JsonGenerator.Feature for configuring low-level JSON generation:

// to force escaping of non-ASCII characters:
// 翻译: 强制转义非 ASCII 字符
mapper.configure(JsonGenerator.Feature.ESCAPE_NON_ASCII, true);
// 将内容包裹为一个 JSON 属性,属性名由 @JsonRootName 注解指定
mapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true);
// 不序列化被 transient 修饰的字段
mapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);

在 Jackson 2.5 版本以后,新增了 enable()disable() 方法可以直接启用或禁用相应的 Feature,推荐使用这种方式。

在 Jackson 2.13 版本以后,ObjectMapper 中的 enable()disable()configure() 都被标记为过时,并且这些方法将在 Jackson 3.0 中被移除,推荐使用 JsonMapper 中的同名方法。

如果想要查看全部的 Features,可以查看 Jackson Features

4.3 常用的 Annotations

Jackson 会根据其默认方式对 Java 对象进行序列化和反序列化,如果需要,可以灵活的调整它的默认方式,可以使用 Jackson 的注解。常用的注解及用法如下:

注解 用法
@JsonProperty 作用于属性,把属性的名称序列化时转换为另外一个名称。
@JsonFormat 作用于属性或者方法,让属性的格式在序列化时转换成指定的格式。
@JsonPropertyOrder 作用于类, 指定属性在序列化时 JSON 中的顺序。
@JsonCreator 作用于构造方法,和 @JsonProperty 配合使用,适用有参数的构造方法。
@JsonAnySetter 作用于属性或者方法,设置未反序列化的属性名和值作为键值存储到 Map 中。
@JsonAnyGetter 作用于方法 ,获取所有未序列化的属性。

@JsonFormat 注解很常用,尤其是在前后端分离的情况下(前后端分离下都是传递 JSON 数据),一般使用它来格式化日期格式与设置时区:

1
2
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
private Date birth;

在使用 @JsonFormat 注解后,当前端提交类似 yyyy-MM-dd 的字符串时,Jackson 能够将其反序列化为 Date 类型的数据。注意,如果提交的是字符串类型的时间戳,则会反序列化失败,但数值类型的时间戳能够序列化成功。

改变属性名

Employee 实体的属性添加 @JsonProperty 注解:

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
static class Employee {
@JsonProperty(value = "employeeName", index = 2)
private String name;
@JsonProperty(index = 1)
private int age;
@JsonProperty(index = 0)
private double height;
}

@JsonProperty 注解的 value 属性用于指定生成的 JSON 字符串字段名,如果不设置,字段名与属性名相同。index 属性可以指定生成的 JSON 字符串字段的顺序,从 0 开始。

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
@Test
@SneakyThrows
public void testAnnotation() {
ObjectMapper mapper = new ObjectMapper();
Employee employee = new Employee("mofan", 19, 178.2);
// 序列化
LinkedHashMap<String, Object> map = mapper.convertValue(employee, new TypeReference<>() {
});
// 转成 LinkedHashMap 进行比较
Map<String, Object> expectMap = new LinkedHashMap<>();
expectMap.put("height", 178.2);
expectMap.put("age", 19);
expectMap.put("employeeName", "mofan");
assertThat(map).containsExactlyEntriesOf(expectMap);
// 反序列化
String str = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(map);
Employee readValue = mapper.readValue(str, Employee.class);
assertThat(readValue).extracting(Employee::getName, Employee::getAge, Employee::getHeight)
.containsExactly("mofan", 19, 178.2);
//language=JSON
String userJson = """
{
"employeeName": "默烦",
"age": 20,
"height": 177.5
}
""";
Employee value = mapper.readValue(userJson, Employee.class);
assertThat(value).extracting(Employee::getName, Employee::getAge, Employee::getHeight)
.containsExactly("默烦", 20, 177.5);
}

属性的忽略

如果想管理在映射 JSON 的时候包含或排除某些属性,可以用到以下几个注解:

  1. @JsonIgnore 作用于属性、方法、构造方法和注解类,用于排除某个属性,这样该属性就不会被 Jackson 序列化和反序列化。
  2. @JsonIgnoreProperties 是类注解。在序列化为 JSON 的时候,@JsonIgnoreProperties({"prop1", "prop2"})会忽略 pro1 和 pro2 两个属性。在从 JSON 反序列化为 Java 实体类的时候,@JsonIgnoreProperties(ignoreUnknown=true) 会忽略所有没有 Getter 和 Setter 的属性。该注解在 Java 类和 JSON 不完全匹配的时候很有用。
  3. @JsonIgnoreType 也是类注解,被注解的类作为别的类的属性时忽略序列化与反序列化。
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
@Test
@SneakyThrows
public void testJsonIgnoreProperties() {
JsonIgnorePropertiesObj obj = new JsonIgnorePropertiesObj();
obj.setIgnore("ignore");
obj.setInteger(212);
obj.setStr("str");
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(obj);
JsonAssert.with(json).assertNotDefined("ignore").assertNotDefined("integer");
assertThat(mapper.readValue(json, JsonIgnorePropertiesObj.class))
.isNotNull()
.hasAllNullFieldsOrPropertiesExcept("str");
}

@Getter
@Setter
@JsonIgnoreProperties({"ignore", "integer"})
static class JsonIgnorePropertiesObj {
private String str;
// @JsonIgnore
private String ignore;
// @JsonIgnore
private Integer integer;
}

@JsonIgnore 与 @JsonProperty 的联合使用

@JsonIgnore 可以作用于方法上,当其作用于某个属性的 Getter 方法时,这个属性在序列化和反序列化时都会被忽略。

现在有一个需求,一个实体中有一名为 pwd 的属性,该属性表示密码,希望在序列化时忽略这个属性,但反序列化的 JSON 字符串中如果有 pwd 字段,则不被忽略。

很容易想到在 pwd 属性的 Getter 方法上添加 @JsonIgnore 注解,但是这样做是不行的,@JsonIgnore 在匹配字段时,Getter 和 Setter 方法之间是共享的,如果只有一个方法具有@JsonIgnore,另一个方法也会被影响。

可以使用“拆分”注释(一般使用 @JsonProperty)来解决这个问题,比如:

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
@Test
@SneakyThrows
public void testJsonIgnore() {
JsonIgnoreObject object = new JsonIgnoreObject();
object.setPwd("123456");
object.setStr("str");
object.setInteger(212);

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(object);
// 序列化的 JSON 中没有 pwd
JsonAssert.with(json).assertNotDefined("$.pwd");
assertThat(mapper.readValue(json, JsonIgnoreObject.class))
.isNotNull()
.extracting(JsonIgnoreObject::getStr, JsonIgnoreObject::getInteger, JsonIgnoreObject::getPwd)
.containsExactly("str", 212, null);

//language=JSON
String newJson = """
{
"str": "str",
"integer": 212,
"password": "987654321"
}
""";
assertThat(mapper.readValue(newJson, JsonIgnoreObject.class))
.extracting(JsonIgnoreObject::getPwd)
.isEqualTo("987654321");
}

@Getter
@Setter
static class JsonIgnoreObject {
private String str;
private Integer integer;
private String pwd;

@JsonIgnore
public String getPwd() {
return pwd;
}

@JsonProperty("password")
public void setPwd(String pwd) {
this.pwd = pwd;
}
}

可以看到,对 JsonIgnoreObject 对象进行序列化时忽略了 pwd 属性,但反序列化 JSON 字符串时,如果字符串中有 password 字段,它的值会被设置到 JsonIgnoreObject 的 pwd 属性上。

序列化相关注解

  • @JsonPropertyOrder 作用于类,用于指定属性序列化时的顺序。@JsonPropertyindex 属性也有类似功能,数字越小越靠前。
  • @JsonRootName 作用于类,用于指定 JSON 根属性的名称。使用这个注解时,必须启用 SerializationFeature.WRAP_ROOT_VALUE 特性。
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
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("root")
@JsonPropertyOrder({"decimal", "string", "integer"})
static class Root {
private String string;
private Integer integer;
private Double decimal;
}

@Test
@SneakyThrows
public void testSerializationAnnotation() {
// 使用 @JsonRootName 注解后,必须启用 SerializationFeature.WRAP_ROOT_VALUE
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.WRAP_ROOT_VALUE);
Root root = new Root("str", 212, 3.14);
String json = mapper.writeValueAsString(root);
//language=JSON
String expectJson = """
{
"root": {
"decimal": 3.14,
"string": "str",
"integer": 212
}
}
""";
JsonAssertions.assertThatJson(json).isEqualTo(expectJson);
// 转成 LinkedHashMap 方便比较顺序(此时最外层不再有 root 根属性)
LinkedHashMap<String, Object> map = mapper.convertValue(root, new TypeReference<>() {
});
Map<String, Object> expectMap = new LinkedHashMap<>();
expectMap.put("decimal", 3.14);
expectMap.put("string", "str");
expectMap.put("integer", 212);
assertThat(map).asInstanceOf(MAP)
.doesNotContainKey("root")
.hasSize(3)
.containsExactlyEntriesOf(expectMap);
}

自定义构造函数

参考链接:java - Cannot construct instance of com.domain.User (no Creators, like default constructor, exist): cannot deserialize from Object value - Stack Overflow

在反序列化时,Jackson 会 默认 使用对象的无参构造方法和对应的 Setter 方法。如果 定义了有参构造方法,没有显式声明无参构造方法,则会在反序列化时抛出 MismatchedInputException 异常,提示没有可用的无参构造方法。

如果想要使用非无参构造方法进行反序列化,可以使用 @JsonCreator 注解指定反序列化时使用的构造方法或者静态工厂方法。

比如要求构造的对象是不可变的,因此在对象所在的类中 提供了全参构造方法,此时就可以使用 @JsonCreator 注解。

注意: @JsonCreator 只能作用在静态方法或构造方法,不能在实例方法上使用,因为那时还没构造出对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@FieldNameConstants
record Student(String stuName, int age, double height, JacksonBasicTest.Student.School school) {
@JsonCreator
Student(@JsonProperty(Fields.stuName) String stuName,
@JsonProperty(Fields.age) int age,
@JsonProperty(Fields.height) double height,
@JsonProperty(Fields.school) School school) {
this.stuName = stuName;
this.age = age;
this.height = height;
this.school = school;
}

@FieldNameConstants
record School(String name) {
@JsonCreator
School(@JsonProperty(Fields.name) String name) {
this.name = name;
}
}
}

类中的字段都是不可变的,没有提供无参构造方法,但是有 @JsonCreator 注解的有参构造方法。

注意: @JsonCreator 需要和 @JsonProperty 注解搭配使用,且 @JsonProperty 注解必须指定 value 属性值。@JsonProperty 注解搭配 Lombok 的 @FieldNameConstants 注解一起“食用”更佳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
@SneakyThrows
public void testJsonCreator() {
ObjectMapper mapper = new ObjectMapper();
String json = """
{
"stuName": "默烦",
"age": 20,
"height": 177.5,
"school": {
"name": "Test"
}
}
""";
Student student = mapper.readValue(json, Student.class);
assertThat(student).extracting(
Student::stuName, Student::age,
Student::height, i -> i.school().name()
).containsExactly("默烦", 20, 177.5, "Test");
}

另外,类似的还有一个 @java.beans.ConstructorProperties 注解(由 JDK 提供),不过只能用在构造方法里,但该注解可以单独使用,无需搭配 @JsonProperty 注解。

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
@Test
@SneakyThrows
public void testConstructorProperties() {
ObjectMapper mapper = new ObjectMapper();
String json = """
{
"name": "mofan",
"personAge": 20
}
""";
Person person = mapper.readValue(json, Person.class);
assertThat(person).extracting(Person::getPersonName, Person::getPersonAge)
.containsExactly("mofan", 20);
}

@Getter
static class Person {
private final String personName;
private final int personAge;

@ConstructorProperties({"name", "personAge"})
public Person(String name, int personAge) {
this.personName = name;
this.personAge = personAge;
}
}

@ConstructorProperties 注解的指定的字符串数组与构造方法参数列表一一对应。

比如,@ConstructorProperties({"name", "personAge"}) 中的 name 表示将 JSON 中的 name 属性值反序列化为 Person 构造方法中的第一个参数值。

修改构造方法为如下形式,运行测试方法也能得到相同的结果:

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

5. 数据类型转换

5.1 类型转换

Jackson 有一项很流弊(但并不出名)的功能,就是能够进行任意的 POJO 到 POJO 的转换,其底层实现逻辑是现将原对象转换成 JSON 信息,然后再将 JSON 信息绑定到另一个对象上。

这样的转换有两个步骤:

  1. 将 POJO 写为 JSON
  2. 将 JSON 绑定到另一种 POJO 中。

只需:

1
ResultType result = mapper.convertValue(sourceObject, ResultType.class);

尝试一下:

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
@Test
@SneakyThrows
public void testConversions() {
ObjectMapper mapper = new ObjectMapper();
// List<Integer> to int[]
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
assertThat(list).containsExactly(1, 2, 3);
int[] ints = mapper.convertValue(list, int[].class);
assertThat(ints).containsExactly(1, 2, 3);
// POJO to Map
User user = new User("mofan", 19, 178.3);
Map<String, Object> map = mapper.convertValue(user, new TypeReference<>() {
});
assertThat(map).containsExactlyInAnyOrderEntriesOf(Map.of(
"age", 19,
"name", "mofan",
"height", 178.3
));
// Map to POJO
User pojo = mapper.convertValue(map, User.class);
assertThat(pojo).extracting(User::getName, User::getAge, User::getHeight)
.containsExactly("mofan", 19, 178.3);
// decode Base64
String base64 = "bW9mYW4=";
byte[] bytes = mapper.convertValue(base64, byte[].class);
String decode = new String(bytes, StandardCharsets.UTF_8);
assertThat(decode).isEqualTo("mofan");

// POJO 转 JsonNode
User people = new User("默烦", 19, 177.5);
JsonNode jsonNode = mapper.convertValue(people, JsonNode.class);
String expectJson = """
{
"name": "默烦",
"age": 19,
"height": 177.5
}
""";
JsonAssertions.assertThatJson(mapper.writeValueAsString(jsonNode)).isEqualTo(expectJson);
}

🐮 大了,有没有?

甚至还能解码 Base64,如果不知道什么是 Base64,可以在本站搜索【Base64 编码的那些事】查看。

5.2 解析为 Long 类型

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
@Test
@SneakyThrows
public void testLongValue() {
String withLong = """
{
"id1": "98765432112345",
"id2": "98765432112345L",
"id3": 98765432112345,
"id4": "1001",
"id5": "1001L",
"id6": 1001
}
""";
ObjectMapper mapper = new ObjectMapper();
ObjectNode objectNode = ((ObjectNode) mapper.readTree(withLong));
JsonNode id1 = objectNode.get("id1");
assertThat(id1.isLong()).isFalse();
JsonNode id2 = objectNode.get("id2");
assertThat(id2.isLong()).isFalse();
JsonNode id3 = objectNode.get("id3");
assertThat(id3.isLong()).isTrue();
JsonNode id4 = objectNode.get("id4");
assertThat(id4.isLong()).isFalse();
JsonNode id5 = objectNode.get("id5");
assertThat(id5.isLong()).isFalse();
JsonNode id6 = objectNode.get("id6");
assertThat(id6.isLong()).isFalse();
}

针对整型数字转换时,如果数字小于 Integer.MAX_VALUE 就会转换成 Integer,而不是 Long

6. 建造者模式

6.1 简单使用

有时会使用建造者模式来构造对象,那如何使用 Jackson 将给定 JSON 信息反序列化成使用了建造者模式的类对应的对象呢?

需要使用到两个注解:

  • @JsonDeserialize:使用 builder 指定使用哪个建造者进行反序列化
  • @JsonPOJOBuilder:标记具体的建造者。默认情况下,要求建造者的建造方法名为 build,设置值的方法前缀是 with
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
@Getter
@Builder
@JsonDeserialize(builder = Person.Builder.class)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
static class Person {
private final String name;
private final Integer age;

// 并没有提供 builder() 方法

@JsonPOJOBuilder
static class Builder {
private String name;
private Integer age;

Builder withName(String name) {
this.name = name;
return this;
}

Builder withAge(Integer age) {
this.age = age;
return this;
}

public Person build() {
return new Person(name, age);
}
}
}

private static final String JSON = """
{
"name": "mofan",
"age": 21
}
""";

@Test
@SneakyThrows
public void testSimplyUse() {
Person person = new ObjectMapper().readValue(JSON, Person.class);
assertThat(person).extracting(Person::getName, Person::getAge)
.containsExactly("mofan", 21);
}

6.2 与 Lombok 结合

实际开发过程中常常使用 Lombok 的 @Builder 注解来实现建造者模式,此时 Jackson 怎么与其结合呢?

Lombok 提供了 @Jacksonized 注解来处理这种情况,它是 @Builder@SuperBuilder 的附加注解,也就是说 @Jacksonized 必须与 @Builder@SuperBuilder 注解一起使用才有效,@Jacksonized 注解能对 Lombok 生成的建造者类进行一些配置,以供 Jackson 反序列化使用。

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
@Getter
@Jacksonized
@Builder(builderMethodName = "hiddenBuilder")
@AllArgsConstructor(access = AccessLevel.PRIVATE)
static class Student {
@JsonProperty("studentName")
@JsonAlias({"stuName", "stu_name"})
private final String name;
private final Integer age;

public static StudentBuilder builder(String name) {
return hiddenBuilder().name(name);
}
}

private static final String FIRST_STUDENT_JSON = """
{
"studentName": "mofan",
"age": 21
}
""";

private static final String SECOND_STUDENT_JSON = """
{
"stu_name": "默烦",
"age": 0
}
""";

@Test
@SneakyThrows
public void testUseWithLombok() {
ObjectMapper mapper = new ObjectMapper();
assertThat(mapper.readValue(FIRST_STUDENT_JSON, Student.class))
.extracting(Student::getName, Student::getAge)
.containsExactly("mofan", 21);
// 测试别名
assertThat(mapper.readValue(SECOND_STUDENT_JSON, Student.class))
.extracting(Student::getName, Student::getAge)
.containsExactly("默烦", 0);
}

@Jacksonized 做了下列这些事:

  • 配置 Jackson 反序列化使用的建造者类,即自动在原始类上添加 @JsonDeserialize 注解,并指定其 builder 的值为 Lombok 生成的建造者类;
  • 将原始类上与 Jackson 相关的注解复制到建造者类上,比如 @JsonProperty@JsonAlias 等注解,以便 Jackson 在使用建造者类时能够识别到它们;
  • 插入 @JsonPOJOBuilder(withPrefix="") 注解在构造器类上,覆盖默认使用 with 作为设置值方法的前缀的设置。如果配置了 @Builder 注解的 setterPrefix 值或 buildMethodName 值,这些配置也会同步到插入;
  • @SuperBuilder 注解,将构造器的实现类设置为 private

额外补充一点:如果 JSON 信息中的字段名与目标对象中的字段名不一样,@JsonProperty 注解应该标记在建造者类的字段上,而不是原始类的字段上,除此之外,使用 @JsonAlias 注解设置字段别名时,该字段必须已经被 @JsonProperty 标记。

7. 不仅仅是 JSON

Jackson 除了用于处理 JSON 外,还提供了一系列拓展用来处理不同类型的配置文件,比如 YAML、XML、BSON、Properties、TOML 等等,使用时需要引入额外的依赖,本节以最常见的 YAML 与 XML 为例。

7.1 处理 YAML

YAML 是 JSON 的超集,Jackson 既然能够解析 JSON,那么 YAML 也自然不在话下。

1
2
3
4
5
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>

GitHub 仓库 README:FasterXML/jackson-dataformats-text

处理方式与处理 JSON 类似,只不过使用的不再是 ObjectMapper,而是其子类 YAMLMapper(其实处理 JSON 使用的也是 ObjectMapper 的子类,名为 JsonMapper)。

比如:

1
2
3
4
5
6
// 1.
ObjectMapper mapper = new YAMLMapper();
// 2.
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
// 3.
ObjectMapper mapper = YAMLMapper.builder().build();

写入与读取

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
private static final File YAML_FILE = new File("test.yaml");

@Getter
@Setter
static class YamlObject {
private String str;
private Integer integer;
}

@Test
@Order(1)
@SneakyThrows
public void testWrite() {
ObjectMapper mapper = new YAMLMapper();
YamlObject object = new YamlObject();
object.setStr("string");
object.setInteger(212);
// 写入 test.yaml 文件中
mapper.writeValue(YAML_FILE, object);
assertThat(YAML_FILE).isNotEmpty();
}

@Test
@Order(2)
@SneakyThrows
public void testRead() {
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
YamlObject value = mapper.readValue(YAML_FILE, YamlObject.class);
assertThat(value).extracting(YamlObject::getStr, YamlObject::getInteger)
.containsExactly("string", 212);
}

7.2 处理 XML

处理 XML 也需要先导入 Jackson 拓展依赖,同时选择 Woodstox 作为 XML 类库,而不是 JDK 自身提供的实现:

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson.version}</version>
</dependency>

<!-- 使用 Woodstox 作为 XML 类库-->
<dependency>
<groupId>com.fasterxml.woodstox</groupId>
<artifactId>woodstox-core</artifactId>
<version>6.5.1</version>
</dependency>

GitHub 仓库:FasterXML/jackson-dataformat-xml

处理 XML 使用的也是 ObjectMapper 的子类,名为 XmlMapper,构造 XmlMapper 的方式也有很多,但更推荐以下更简单易用的方式:

1
2
3
4
// 1.
ObjectMapper mapper = new XmlMapper();
// 2.
ObjectMapper mapper = XmlMapper.builder().build();

至于 new ObjectMapper(new XmlFactory()) 的创建方式无法做到开箱即用,需要进行额外的配置。

写入与读取

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
private static final File XML_FILE = new File("test.xml");

@Getter
@Setter
static class XmlObject {
private String string;
private Integer integer;
}

@Test
@SneakyThrows
public void testWrite() {
ObjectMapper mapper = new XmlMapper();
XmlObject object = new XmlObject();
object.setString("string");
object.setInteger(212);
mapper.writeValue(XML_FILE, object);
assertThat(XML_FILE).isNotEmpty();
}

@Test
@SneakyThrows
public void testRead() {
ObjectMapper mapper = XmlMapper.builder().build();
XmlObject value = mapper.readValue(XML_FILE, XmlObject.class);
assertThat(value).extracting(XmlObject::getString, XmlObject::getInteger)
.containsExactly("string", 212);
}

test.xml 的文件内容如下:

1
<XmlObject><string>string</string><integer>212</integer></XmlObject>

相关的几个注解

  • @JacksonXmlRootElementnamespace 属性用于指定 XML 标签中 xmlns 的值,显式设置 namespace 的值后,其内部子标签都将增加 xmlns 属性。localName 属性指定 XML 标签的名称。

  • @JacksonXmlPropertynamespace 属性用于指定 XML 标签中 xmlns 的值。localName 属性指定 XML 标签的名称。isAttribute 指定当前字段是否作为父标签的属性,默认为 false,表示作为父标签的子标签。

  • @JacksonXmlText:将字段值直接作为普通文本。

  • @JacksonXmlCData:将字段值包裹在 CDATA 标签中。

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
@Getter
@Setter
@JacksonXmlRootElement(localName = "Root")
static class AnnotationTestObject {
private String string;
private Integer integer;
private Double decimal;
@JacksonXmlText
private String str;
@JacksonXmlCData
private String code;
@JacksonXmlProperty(isAttribute = true)
@JsonFormat(pattern = "yyyy-MM-dd")
private Date date;
}

@Test
@SneakyThrows
public void testAnnotation() {
AnnotationTestObject object = new AnnotationTestObject();
object.setString("string");
object.setInteger(212);
object.setDecimal(3.14);
object.setStr("str");
object.setCode("hello world!");
object.setDate(new Date(1672502400000L));

XmlMapper mapper = XmlMapper.builder()
// 格式化输出
.enable(SerializationFeature.INDENT_OUTPUT)
.build();
File file = new File("xml-annotation.xml");
mapper.writeValue(file, object);
assertThat(file).isNotEmpty();
}

xml-annotation.xml 的文件内容如下:

1
2
3
4
5
6
<Root date="2022-12-31">
<string>string</string>
<integer>212</integer>
<decimal>3.14</decimal>str
<code><![CDATA[hello world!]]></code>
</Root>

8. 更多注解

注解 含义
@JacksonAnnotation Jackson 注解的元注解,用于标记当前注解是 Jackson 注解的一部分
@JacksonAnnotationsInside 将多个 Jackson 注解组合成一个自定义注解

9. 杂项补充

9.1 JsonAutoDetect

默认情况下,Jackson 使用 public 的字段、public 的 Getter/Setter 进行序列化或反序列化。

这种可见性规则是可以显示指定的,比如在第三方类库中很可能存在不符合默认序列化要求的类,但又想让其能够被序列化或反序列化,此时可以显示指定 Jackson 的 JsonAutoDetect 可见性规则。

1
2
3
4
5
6
7
8
9
10
11
static class Student {
private String name;
private Integer age;

public static Student of(String name, Integer age) {
Student student = new Student();
student.name = name;
student.age = age;
return student;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
@SneakyThrows
public void testJsonAutoDetect() {
ObjectMapper mapper = new ObjectMapper();
// 也可以在目标类上使用 @JsonAutoDetect 注解
mapper.setVisibility(
mapper.getVisibilityChecker()
// 利用反射,不依赖 Getter/Setter
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
);

Student student = Student.of("默烦", 21);
String json = mapper.writeValueAsString(student);
JsonAssert.with(json)
.assertEquals("name", "默烦")
.assertEquals("age", 21);

// 反序列化时必须要求有无参构造或者 Jackson 注解显式指定的用于反序列化的构造器
assertThat(mapper.readValue(json, Student.class))
.extracting("name", "age")
.containsExactly("默烦", 21);
}

9.2 属性忽略

在序列化或反序列化过程中经常会遇到忽略某种或某个属性的情况,在此对这种情况的实现进行一个总结。

只序列化不为 null 的属性

1
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

反序列化时忽略在 JSON 中存在、而在 Java 类中不存在的属性

1
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

利用注解简单实现属性忽略

  • @JsonIgnore:使被标记的字段或属性不会被序列化、反序列化
  • @JsonIgnoreProperties:使指定的多个属性不会被序列化、反序列化
  • @JsonIgnoreType:使某种类型不会被序列化或反序列化

使用方式可以参考:JsonIgnoreTest.java

利用 @JsonFilter 实现序列化或反序列化的单一属性忽略

某些情况下可能只要求在序列化(或反序列化)时进行属性忽略,而在反序列化(或序列化)时不进行属性忽略,此时可以使用 @JsonFilter 指定自定义的过滤器。

比如要求不序列化 PersonidCardNum 字段、Personcompany 字段的 location 字段,需要先使用 @JsonFilter 注解指定过滤器名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Getter
@Setter
@JsonFilter("idCardNumFilter")
static class Person {
private String name;
private Integer age;
private String idCardNum;
@JsonIgnore
private String otherSensitiveInfo;
@JsonFilter("locationFilter")
private Company company;
}

@Getter
@Setter
static class Company {
private String name;
private String location;
}

对于指定的过滤器名称,要求在 ObjectMapper 对象中一定要存在同名的过滤器,否则运行时抛出 JsonMappingException 异常,提示没有配置指定名称的过滤器。

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
@Test
@SneakyThrows
public void testJsonFilter() {
SimpleFilterProvider filterProvider = new SimpleFilterProvider();
// 序列化时忽略 idCardNum 字段
filterProvider.addFilter("idCardNumFilter", SimpleBeanPropertyFilter.serializeAllExcept("idCardNum"))
.addFilter("locationFilter", SimpleBeanPropertyFilter.serializeAllExcept("location"));
ObjectMapper mapper = new ObjectMapper();
mapper.setFilterProvider(filterProvider);

Person person = new Person();
person.setName("默烦");
person.setAge(21);
person.setIdCardNum("XXX");
person.setOtherSensitiveInfo("ABC");
Company company = new Company();
company.setName("百年不倒股份有限公司");
company.setLocation("Have a guess");
person.setCompany(company);

String json = mapper.writeValueAsString(person);
JsonAssert.with(json).assertNotDefined("idCardNum")
.assertNotDefined("otherSensitiveInfo")
.assertNotDefined("$.company.location")
.assertThat("$.name", equalTo("默烦"))
.assertThat("$.age", equalTo(21));


//language=JSON
String deserializedJson = """
{
"name": "默烦",
"age": 21,
"idCardNum": "XXX",
"otherSensitiveInfo": "ABC",
"company": {
"name": "百年不倒股份有限公司",
"location": "UNKNOWN"
}
}
""";
Person newPerson = mapper.readValue(deserializedJson, Person.class);
assertThat(newPerson).hasNoNullFieldsOrPropertiesExcept("otherSensitiveInfo")
.extracting("idCardNum", "company.location")
.containsExactly("XXX", "UNKNOWN");
}

直接在目标类上添加注解来实现属性忽略将作用于全局,而在某些极端场景下要求只在该场景进行属性忽略,此时可以使用 Jackson 的 Mix-in 特性来实现。

1
2
3
4
5
6
7
8
@Getter
@Setter
static class SimpleObj {
private String a;
private String b;
private String c;
private String d;
}

比如只要求不序列化 cd 字段,对 ab 字段依旧照常序列化,那么可以先定义 Mix-in 类,在该类上使用 @JsonFilter 指定过滤器,而后使 MixIn 类“混入”目标类 SimpleObj 以实现这种极端场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@JsonFilter("selectiveFilter")
static class SelectiveFilterMixIn {
}

@Test
@SneakyThrows
public void testSelectiveFilter() {
SimpleObj obj = new SimpleObj();
obj.setA("a");
obj.setB("b");
obj.setC("c");
obj.setD("d");

ObjectMapper mapper = new ObjectMapper();
SimpleFilterProvider provider = new SimpleFilterProvider();
provider.addFilter("selectiveFilter", SimpleBeanPropertyFilter.serializeAllExcept("c", "d"));
mapper.setFilterProvider(provider)
.addMixIn(SimpleObj.class, SelectiveFilterMixIn.class);
JsonAssert.with(mapper.writeValueAsString(obj))
.assertNotDefined("c")
.assertNotDefined("d")
.assertEquals("a", "a")
.assertEquals("b", "b");
}

9.3 Mix-in

在【8.2 属性忽略】中首次接触到了 Mix-in,Mix-in 可直译为“混入”,是一种将注解与类向关联的方式,而无需修改目标类本身,旨在帮助用户将某些 Jackson 的注解添加到无法修改源码的第三方类库中的类上。

可供参考的官方链接:JacksonMixInAnnotations · FasterXML/jackson-docs Wiki

序列化第三方类库中的类

除了修改 Jackson 的 JsonAutoDetect 可见性规则来使第三方类库中的类可被序列化之外,还可以使用 Mix-in 实现。

假设有一第三方类库中的类为 Person,所有字段均为 private,并且没有提供任何公共的 Getter/Setter,也没有无参构造,显然无法使用 Jackson 的默认序列化规则对其进行序列化或反序列化:

1
2
3
4
5
6
7
8
9
10
static class Person {

private final String firstName;
private final String lastName;

public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}

创建 Person 的 Mix-in 类,使它具有与 Person 相同的字段,使用合适的注解指定其被序列化或反序列化使用的信息:

1
2
3
4
5
6
7
8
9
10
11
12
abstract static class PersonMixIn {
@JsonProperty
private final String firstName;
@JsonProperty
private final String lastName;

@JsonCreator
public PersonMixIn(@JsonProperty("firstName") String firstName, @JsonProperty("lastName") String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}

接下来关联 Person 类与其 Mix-in 类,使得 Person 能够被序列化或反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
@SneakyThrows
public void testSerializeThirdPartyObject() {
Person person = new Person("默", "烦");
ObjectMapper mapper = new ObjectMapper();
assertThatThrownBy(() -> mapper.writeValueAsString(person))
.isInstanceOf(InvalidDefinitionException.class);

// configure-first-then-use,new 一个新的 ObjectMapper 对象
ObjectMapper anotherMapper = new ObjectMapper();
anotherMapper.addMixIn(Person.class, PersonMixIn.class);
String json = anotherMapper.writeValueAsString(person);
JsonAssert.with(json).assertThat("$.firstName", equalTo("默"))
.assertThat("$.lastName", equalTo("烦"));
Person newPerson = anotherMapper.readValue(json, Person.class);
assertThat(newPerson).extracting("firstName", "lastName")
.containsExactly("默", "烦");
}

序列化时忽略、改变第三方类库中类的属性

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
@Getter
@Setter
static class Student {
private String name;
private String idCardNum;
}

abstract static class StudentMixIn {
@JsonProperty("NAME")
private String name;
@JsonIgnore
private String idCardNum;
}

@Test
@SneakyThrows
public void testIgnoreAndChangeThirdPartyObjectProperties() {
Student student = new Student();
student.setName("默烦");
student.setIdCardNum("xxx");

ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Student.class, StudentMixIn.class);
String json = mapper.writeValueAsString(student);
JsonAssert.with(json).assertNotDefined("idCardNum")
.assertThat("$.NAME", equalTo("默烦"));
Student newStu = mapper.readValue(json, Student.class);
assertThat(newStu).extracting("name").isEqualTo("默烦");
}

修改自定义序列化器、反序列化器

某些情况下要求某个类在序列化或反序列化时不使用其默认的序列化器或反序列化器,这种需求也可以使用 Mix-in 实现。

比如 People 默认的序列化器是 PeopleSerializer,默认的反序列化器是 PeopleDeserializer

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
private static final String SEPARATOR = "->";

@Getter
@EqualsAndHashCode(of = {"realName", "nickName"})
@JsonSerialize(using = People.PeopleSerializer.class)
@JsonDeserialize(using = People.PeopleDeserializer.class)
static class People {
private final String realName;
private final String nickName;

@JsonCreator
public People(@JsonProperty String realName, @JsonProperty String nickName) {
this.realName = realName;
this.nickName = nickName;
}

static class PeopleSerializer extends JsonSerializer<People> {
@Override
public void serialize(People people, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
// 序列化时,先真实姓名再昵称
jsonGenerator.writeString(people.getRealName() + SEPARATOR + people.getNickName());
}
}

static class PeopleDeserializer extends JsonDeserializer<People> {
@Override
public People deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
String[] split = jsonParser.getValueAsString().split(SEPARATOR);
return new People(split[0], split[1]);
}
}
}

现在要求修改这种默认配置,而使用用户自定义的配置,同样是创建对应的 Mix-in 类(或接口),然后添加自定义配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@JsonSerialize(using = PeopleMixIn.AnotherPeopleSerializer.class)
@JsonDeserialize(using = PeopleMixIn.AnotherPeopleDeserializer.class)
static abstract class PeopleMixIn {
static class AnotherPeopleSerializer extends JsonSerializer<People> {
@Override
public void serialize(People people, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
// 序列化时,先昵称再真实姓名
jsonGenerator.writeString(people.getNickName() + SEPARATOR + people.getRealName());
}
}

static class AnotherPeopleDeserializer extends JsonDeserializer<People> {
@Override
public People deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
String[] split = jsonParser.getValueAsString().split(SEPARATOR);
return new People(split[1], split[0]);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
@SneakyThrows
public void testChangeCustomSerializer() {
People people = new People("默烦", "mofan212");

ObjectMapper mapper = new ObjectMapper();
String str = mapper.writeValueAsString(people);
assertThat(str).isEqualTo("\"默烦->mofan212\"");
People newPeople = mapper.readValue(str, People.class);
assertThat(newPeople).isEqualTo(people);

mapper = new ObjectMapper();
mapper.addMixIn(People.class, PeopleMixIn.class);
str = mapper.writeValueAsString(people);
assertThat(str).isEqualTo("\"mofan212->默烦\"");
newPeople = mapper.readValue(str, People.class);
assertThat(newPeople).isEqualTo(people);
}

9.4 序列化中的循环引用

当 A 对象依赖 B 对象,B 对象有依赖 A 对象,且相同对象都指向同一个内存地址时,此时对 A 对象进行序列化就会产生 StackOverflowError 错误,要想解决这个问题,就需要切断对象间的循环引用,可以使用这几个方式:

  1. 序列化时忽略相互引用的字段
  2. 使用 @JsonIdentityInfo 注解序列化循环引用的标识符,而不是序列化完整对象
  3. 使用 @JsonBackReference 注解指定引用关系

忽略相互引用的字段

使用 @JsonIgnoreProperties 注解忽略相互引用的字段:

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
@Getter
@Setter
static class User {
private String username;
private String pwd;
private Role role;
}

@Getter
@Setter
static class Role {
private String name;
@JsonIgnoreProperties("role")
private List<User> users;
}

@Test
@SneakyThrows
public void testCircularReference() {
User user = new User();
user.setUsername("mofan");
user.setPwd("123456");

Role role = new Role();
role.setName("admin");
role.setUsers(List.of(user));

user.setRole(role);

JsonMapper mapper = JsonMapper.builder().build();
String json = mapper.writeValueAsString(user);
// language=JSON
String expectJson = """
{
"username": "mofan",
"pwd": "123456",
"role": {
"name": "admin",
"users": [
{
"username": "mofan",
"pwd": "123456"
}
]
}
}
""";
JsonAssertions.assertThatJson(json).isEqualTo(expectJson);
}

这样做尽管解决了循环引用,但再次对序列化得到的 JSON 信息进行反序列化时,会造成信息丢失。

使用 @JsonIdentityInfo 注解

使用 @JsonIdentityInfo 注解后,在序列化过程中遇到相同引用的对象时,将使用指定的标识符代替该对象,而不是将该对象完整地序列化。

  • 一对一:

    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
    @Getter
    @Setter
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
    static class Customer {
    private Long id;
    private String name;
    private Order order;
    }

    @Getter
    @Setter
    static class Order {
    private Long orderId;
    private List<Integer> itemIds;
    private Customer customer;
    }

    @Test
    @SneakyThrows
    public void testSimplyUse() {
    Order order = new Order();
    order.setOrderId(1L);
    order.setItemIds(List.of(10, 30));

    Customer customer = new Customer();
    customer.setId(2L);
    customer.setName("Peter");
    // customer 依赖 order
    customer.setOrder(order);
    // order 又依赖 custom,出现依赖循环
    order.setCustomer(customer);

    JsonMapper mapper = JsonMapper.builder().build();
    // 序列化
    String json = mapper.writeValueAsString(customer);
    // language=json
    String expectJson = """
    {
    "id": 2,
    "name": "Peter",
    "order": {
    "orderId": 1,
    "itemIds": [10, 30],
    "customer": 2
    }
    }
    """;
    // order 虽然依赖了 customer,但在使用 @JsonIdentityInfo 注解后,以指定的 id 作为代替
    JsonAssertions.assertThatJson(json).isEqualTo(expectJson);

    Customer newCustom = mapper.readValue(json, Customer.class);
    Assertions.assertThat(newCustom).isEqualTo(newCustom.getOrder().getCustomer()).extracting(
    Customer::getId, Customer::getName,
    i -> i.getOrder().getOrderId(), i -> i.getOrder().getItemIds()
    ).containsSequence(2L, "Peter", 1L, List.of(10, 30));
    Assertions.assertThat(newCustom).extracting(i -> i.getOrder().getCustomer())
    .extracting(Customer::getId, Customer::getName)
    .containsSequence(2L, "Peter");
    }
  • 一对多:

    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
    @Getter
    @Setter
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "empId")
    static class Employee {
    private Long empId;
    private List<Dept> depts;
    }

    @Getter
    @Setter
    static class Dept {
    private Long deptId;
    private List<Employee> employees;
    }

    @Test
    @SneakyThrows
    public void testOne2Many() {
    Employee employee = new Employee();
    employee.setEmpId(1L);

    Dept dept1 = new Dept();
    dept1.setDeptId(11L);
    dept1.setEmployees(Collections.singletonList(employee));

    Dept dept2 = new Dept();
    dept2.setDeptId(12L);
    dept2.setEmployees(Collections.singletonList(employee));

    employee.setDepts(List.of(dept1, dept2));

    JsonMapper mapper = JsonMapper.builder().build();
    String json = mapper.writeValueAsString(employee);
    // language=json
    String expectJson = """
    {
    "empId": 1,
    "depts": [
    {
    "deptId": 11,
    "employees": [1]
    },
    {
    "deptId": 12,
    "employees": [1]
    }
    ]
    }
    """;
    JsonAssertions.assertThatJson(json).isEqualTo(expectJson);

    Employee newEmp = mapper.readValue(json, Employee.class);
    Assertions.assertThat(newEmp).extracting(
    i -> i.getDepts().get(0).getEmployees().get(0).getEmpId(),
    i -> i.getDepts().get(1).getEmployees().get(0).getEmpId()
    ).containsSequence(1L, 1L);
    }

@JsonIdentityInfo 注解中的 generator 除了指定为 PropertyGenerator 外,还有其他生成器可供选择,比如:

  • StringIdGenerator
  • UUIDGenerator
  • IntSequenceGenerator

当然也可以自定义生成器。

使用 IntSequenceGenerator 作为生成器处理自引用关系:

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
@Getter
@Setter
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@key")
static class Entity {
private String name;
private List<Entity> entities;
}

@Test
@SneakyThrows
public void testIntSequenceGenerator() {
Entity entity = new Entity();
entity.setName("Root");

Entity firstSubEntity = new Entity();
firstSubEntity.setName("SubEntity-1");
Entity secondSubEntity = new Entity();
secondSubEntity.setName("SubEntity-2");

entity.setEntities(List.of(entity, firstSubEntity, secondSubEntity));

JsonMapper mapper = JsonMapper.builder().build();
String expectJson = """
{
"@key": 1,
"name": "Root",
"entities": [
1,
{
"@key": 2,
"name": "SubEntity-1",
"entities": null
},
{
"@key": 3,
"name": "SubEntity-2",
"entities": null
}
]
}
""";
JsonAssertions.assertThatJson(mapper.writeValueAsString(entity))
.isEqualTo(expectJson);
}

使用 @JsonBackReference 注解

使用 @JsonBackReference 注解后进行序列化时,得到的结果与使用了 @JsonIgnore 类似,而与 @JsonManagedReference 注解联合使用指定引用关系时,对先前序列化的结果进行反序列化后,得到的对象信息并不会缺失。

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
@Getter
@Setter
static class Employer {
private String name;
@JsonManagedReference
private List<Employee> employees;
}

@Getter
@Setter
static class Employee {
private String name;
@JsonBackReference
private Employer employer;
}

@Test
@SneakyThrows
public void test() {
Employee employee = new Employee();
employee.setName("mofan");

Employer employer = new Employer();
employer.setName("默烦");
employer.setEmployees(List.of(employee));

employee.setEmployer(employer);

JsonMapper mapper = JsonMapper.builder().build();
String json = mapper.writeValueAsString(employer);
String expectJson = """
{
"name": "默烦",
"employees": [
{
"name": "mofan"
}
]
}
""";
// 效果与使用了 @JsonIgnore 类似,没有序列化被 @JsonBackReference 标记的字段
JsonAssertions.assertThatJson(json).isEqualTo(expectJson);

// 反序列化呢?在 Employer 中的 employees 上使用 @JsonManagedReference 注解
Employer value = mapper.readValue(expectJson, Employer.class);
assertThat(value.getEmployees()).map(Employee::getEmployer)
.filteredOn(Objects::nonNull)
.hasSize(1)
.map(Employer::getName)
.singleElement()
.isEqualTo("默烦");
}

9.5 对 JDK8 日期时间的支持

Jackson 也对 JDK8 新增的日期时间类提供了支持,这些支持以 Jackson 模块的形式提供,因此需要导入对应的依赖:

1
2
3
4
5
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>

导入依赖后并不是就万事大吉了,还要在使用时注册这些模块。

可以按需单个注册:

1
mapper.addModule(new JavaTimeModule());

也可以让 Jackson 自己查找并注册所有模块:

1
mapper.findAndRegisterModules();

简单测试下:

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
@Getter
@Setter
static class TimeObject {
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate localDate;
}

@Test
@SneakyThrows
public void testSupportDateTime() {
TimeObject object = new TimeObject();
object.setLocalDate(LocalDate.of(2023, 1, 1));

assertThatThrownBy(() -> new ObjectMapper().writeValueAsString(object))
.isExactlyInstanceOf(InvalidDefinitionException.class);

ObjectMapper mapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.build();
String expectJson = """
{
"localDate": "2023-01-01"
}
""";
JsonAssertions.assertThatJson(mapper.writeValueAsString(object))
.isEqualTo(expectJson);
}

9.6 传播 transient

Java 提供了 transient 关键字用来修饰无需被序列化的字段,但在默认情况下,Jackson 必不认这个关键字,除非开启 MapperFeature.PROPAGATE_TRANSIENT_MARKER 的配置。

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
@Getter
@Setter
public class Phone implements Serializable {
@Serial
private static final long serialVersionUID = -2025192839395117683L;

public static String staticStr;
private String name;
private transient BigDecimal price;
}

@Test
@SneakyThrows
public void testSerializedPhoneByJackson() {
Phone phone = new Phone();
phone.setName("XiaoMi");
phone.setPrice(new BigDecimal("1999.0"));
Phone.staticStr = "Hello, Thank you";
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(phone);

String except = """
{
"name": "XiaoMi"
}
""";
assertThatJson(json)
.isObject()
.isNotEqualTo(except)
.containsEntry("price", new BigDecimal("1999.0"))
// 静态字段不会被反序列化
.doesNotContainKey("staticStr");

// 不序列化被 transient 修饰的字段
mapper = JsonMapper.builder().configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true).build();
json = mapper.writeValueAsString(phone);
assertThatJson(json).isEqualTo(except);
}

针对静态字段来说,Jackson 与 Java 的序列化机制一样,也不会序列化静态字段。

10. 使用细节

10.1 与 Lombok 一起使用可能存在的问题

存在名为 MyFirstData 的类,使用 Lombok 为其生成 Getter/Setter:

1
2
3
4
5
6
@Getter
@Setter
static class MyFirstData {
private String s;
private Integer iAge;
}

将给定字符串反序列化成 MyFirstData 实例:

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
static JsonMapper mapper = JsonMapper.builder().build();

// language=JSON
static final String JSON = """
{
"iAge": 21
}
""";

@Test
@SneakyThrows
public void testDeserializeFirstData() {
// 抛出异常
assertThatExceptionOfType(UnrecognizedPropertyException.class)
.isThrownBy(() -> mapper.readValue(JSON, MyFirstData.class));

// 忽略 JSON 串中存在,Java 类中不存在的字段
JsonMapper jsonMapper = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
try {
MyFirstData data = jsonMapper.readValue(JSON, MyFirstData.class);
assertThat(data).extracting(MyFirstData::getIAge).isNull();
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}

MyFirstData data = new MyFirstData();
data.setIAge(21);

String result = mapper.writeValueAsString(data);
JsonAssertions.assertThatJson(result).isObject()
// 不存在 iAge
.doesNotContainKey("iAge")
// 存在 iage
.containsEntry("iage", 21);
}

第一次反序列化时抛出了异常,提示无法识别 JSON 串中的 iAge 字段,而后加上停用 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 特性再次反序列化时,不再抛出异常,但 MyFirstData 实例中的 iAge 字段值为 null

新建一个 MyFirstData 实例,对其进行序列化,在得到的 JSON 中不包含 iAge 的字段,而是包含名为 iage 的字段,这是为什么?

这是因为 Lombok 生成的 Setter 与预期不符,查看编译生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static class MyFirstData {
private String s;
private Integer iAge;

MyFirstData() {
}

public String getS() {
return this.s;
}

public Integer getIAge() {
return this.iAge;
}

public void setS(String s) {
this.s = s;
}

public void setIAge(Integer iAge) {
this.iAge = iAge;
}
}

iAge 字段生成的 Set 方法名为 setIAge,Jackson 在初始化序列器时,会使用 DefaultAccessorNamingStrategy#legacyManglePropertyName() 方法来处理 Getter/Setter,它会将方法的 get/set 前缀移除,将其后面连续大写字符转换为小写字符返回。

如果使用 IDEA 生成的 Getter/Setter 就不会出现这样的问题:

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
@SuppressWarnings("SpellCheckingInspection")
static class MySecondData {
private Integer iAge;

public Integer getiAge() {
return iAge;
}

public void setiAge(Integer iAge) {
this.iAge = iAge;
}
}

@Test
public void testDeserializeFourthData() {
validDeserializeToTargetClass(MyFourthData.class);
}

@SneakyThrows
private <T> void validDeserializeToTargetClass(Class<T> clazz) {
T data = mapper.readValue(JSON, clazz);
assertThat(data).isNotNull()
.extracting("iAge")
.isEqualTo(21);

String result = mapper.writeValueAsString(data);
JsonAssertions.assertThatJson(result).isObject().isEqualTo(JSON);
}

如果非要使用 Lombok 生成 Getter/Setter,则需要搭配 @JsonProperty 注解使用:

1
2
3
4
5
6
7
8
9
10
11
@Getter
@Setter
static class MyThirdData {
@JsonProperty("iAge")
private Integer iAge;
}

@Test
public void testDeserializeThirdData() {
validDeserializeToTargetClass(MyThirdData.class);
}

编译生成的 MyThirdData 类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static class MyThirdData {
@JsonProperty("iAge")
private Integer iAge;

MyThirdData() {
}

public Integer getIAge() {
return this.iAge;
}

@JsonProperty("iAge")
public void setIAge(Integer iAge) {
this.iAge = iAge;
}
}

字段 iAgesetIAge() 方法上都使用了 @JsonProperty("iAge") 注解。

其实只在 setIAge() 方法上使用 @JsonProperty("iAge") 注解即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter
static class MyFourthData {
private Integer iAge;

@JsonProperty("iAge")
public void setIAge(Integer iAge) {
this.iAge = iAge;
}
}

@Test
public void testDeserializeFourthData() {
validDeserializeToTargetClass(MyFourthData.class);
}