Jackson ObjectMapper的权威指南--序列化和反序列化Java对象

3,800 阅读21分钟

简介

杰克逊是一个强大而高效的Java库,处理Java对象及其JSON表示的序列化和反序列化。它是这项任务中使用最广泛的库之一,并在许多其他框架的引擎盖下运行。例如,虽然Spring框架支持各种序列化/反序列化库,但Jackson是默认引擎。

在当今时代,JSON是迄今为止由RESTFul Web服务产生和消费数据的最常见和最受欢迎的方式,而且这个过程是 工具到所有的Web服务。虽然Java SE没有提供广泛的支持,将JSON转换为Java对象或反过来,但我们有Jackson这样的第三方库来为我们解决这个问题。

如果你想了解另一个有用的Java库Gson,请阅读我们的《用Gson将Java对象(POJO)转换成JSON》指南

也就是说,Jackson是几乎所有从事网络应用的Java软件工程师的 "必知 "工具之一,熟悉/适应它将对你有长远的帮助。

在这个深入的指南中,我们将对Jackson的核心API--ObjectMapper进行深入研究,通过许多实际的例子让你对如何使用这个类有一个全面而详细的认识。然后,我们将看看用于解析任意结构的树模型,接着是自定义标志和编写自定义序列器和反序列器。

安装Jackson

让我们首先把Jackson作为我们项目的一个依赖项。如果你还没有,你可以通过CLI和Maven轻松生成它。

$ mvn archetype:generate -DgroupId=com.stackabuse.tutorial -DartifactId=objectmapper-tutorial -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false

或者,使用Spring Initializr通过GUI创建一个骨架项目。Jackson不是一个内置的依赖项,所以你不能通过CLI或Spring Initializr将其包括在内,不过,包括它就像修改你的pom.xml 文件一样容易。

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.1</version>
</dependency>

或者,如果你使用Gradle作为你的构建工具。

implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.1'

这就安装了两个库:jackson-annotationsjackson-core

ObjectMapper类的介绍

Jackson库中用于读写JSON的主要类是ObjectMapper 。它在com.fasterxml.jackson.databind 包中,可以序列化和反序列化两种类型的对象。

  1. 普通的旧Java对象(POJO)
  2. 通用的JSON树模型

如果你已经有了一个领域类,一个POJO,你可以通过向ObjectMapper ,在该类和JSON之间进行转换。另外,你也可以将任何任意的JSON转换为任意的JSON树模型,以防你没有专门的转换类,或者做一个类是 "不经济的"。

ObjectMapper 类提供了四个构造函数来创建一个实例,下面这个是最简单的一个。

ObjectMapper objectMapper = new ObjectMapper();

下面是ObjectMapper 的一些重要特征。

  • 它是线程安全的。
  • 它可以作为更高级的ObjectReaderObjectWriter 类的工厂。
  • JsonParser 和 对象将被映射器用来实现JSON的实际读写。JsonGenerator

ObjectMapper 中可用的方法非常多,所以让我们开始吧!

将JSON转换为Java对象

可以说,两个最常用的功能之一是将JSON字符串转换为Java对象。这通常是在你收到一个包含JSON序列化实体的响应,并希望将其转换为一个对象以便进一步使用时进行的。

通过ObjectMapper ,将一个JSON字符串转换成一个Java对象,我们使用readValue() 方法。

该方法接受各种各样的数据源,我们将在接下来的章节中进行介绍。

将JSON字符串转换为Java对象(POJO)

最简单的输入形式是String - 或者说,JSON格式的字符串。

<T> T readValue(String content, Class<T> valueType)

考虑一下健康管理系统中的以下HealthWorker 类。

public class HealthWorker {
    private int id;
    private String name;
    private String qualification;
    private Double yearsOfExperience;

    // Constructor, getters, setters, toString()
}

要把这个类的JSON字符串表示法转换成一个Java类,我们只需把这个字符串提供给readValue() 方法,同时提供我们要转换的类的.class

ObjectMapper objectMapper = new ObjectMapper();
String healthWorkerJSON = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";

HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON, HealthWorker.class);

如你所料,healthWorker 对象的name 属性将被设置为 "RehamMuzzamil",qualification 为 "MBBS",yearsOfExperience 为1.5。

注意:字段名必须与JSON字符串中的字段完全匹配,否则映射器会抛出一个错误。此外,它们必须有有效的公共getters和setters。Jackson还支持使用不同名称的别名,可以通过简单的注释将任何JSON字段映射到任何POJO字段。

@JsonAlias 和 @JsonProperty

每当JSON字符串和POJO中的属性/字段名称不匹配时,你可以通过不反序列化它们或 "适应 "哪些JSON字段被映射到哪些对象字段来处理这种不匹配。

这可以通过@JsonAlias@JsonProperty 来实现。

  • @JsonProperty对应于序列化和反序列化过程中的字段名。
  • @JsonAlias对应于反序列化过程中的替代名称。

例如,一个常见的不匹配发生在大写字母的约定上--一个API可能会返回snake_case ,而你期待的是CamelCase

public class HealthWorker {
    private int workerId;
    private String workerName;
    private String workerQualification;
    private Double yearsOfExperience;
    
    // Constructor, getters, setters and toString()
}

而传入的JSON看起来像这样。

{
  "worker_id" : 1,
  "worker_name" : "RehamMuzzamil",
  "worker_qualification" : "MBBS",
  "years_of_experience" :1.5
}

这些都是不被认可的字段,尽管它们显然代表相同的属性这可以通过设置@JsonProperty 注解来轻松避免。

public class HealthWorker {
    @JsonProperty("worker_id")
    private int workerId;
    @JsonProperty("worker_name")
    private String workerName;
    @JsonProperty("worker_qualification")
    private String workerQualification;
    @JsonProperty("years_of_experience")
    private Double yearsOfExperience;
    
    // Constructor, getters, setters and toString()
}

现在在序列化和反序列化时,都会执行蛇的情况,POJO和传入的JSON之间不会出现问题。另一方面,如果你不想在蛇形码中序列化字段,但仍然能够读取它们--你可以选择一个别名来代替传入的蛇形码会被解析成驼色码,但当你序列化时,它仍然会被序列化成驼色码。

此外,你可以同时使用这两个注解!在这种情况下,@JsonAlias ,作为除了强制执行的属性名称之外的替代名称被接受,你甚至可以为注释提供一个列表。

public class HealthWorker {

    @JsonProperty("worker_id")
    @JsonAlias({"id", "workerId", "identification"})
    private int workerId;
    @JsonProperty("worker_name")
    @JsonAlias({"name", "wName"})
    private String workerName;
    @JsonProperty("worker_qualification")
    @JsonAlias({"workerQualification", "qual", "qualification"})
    private String workerQualification;
    @JsonProperty("years_of_experience")
    @JsonAlias({"yoe", "yearsOfExperience", "experience"})
    private Double yearsOfExperience;
    
    // Constructor, getters, setters and toString()
}

现在,任何一个别名都会被映射到同一个属性,但在序列化时,会使用@JsonProperty 的值。你可以通过这种方式将多个API响应映射到一个对象,如果API包含相同的结构性响应,但名称不同,例如。

用读取器将JSON字符串转换为Java对象(POJO)

Reader 类代表了一个任意的字符数据流,可以从像字符串这样的来源构造。readValue() 方法也接受一个Reader ,而不是Strings。

<T> T readValue(Reader src, Class<T> valueType)

其余的代码都是一样的。

ObjectMapper objectMapper = new ObjectMapper();
String healthWorkerJSON = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";
Reader reader = new StringReader(healthWorkerJSON);
HealthWorker healthWorker = objectMapper.readValue(reader, HealthWorker.class);

将JSON文件转换为Java对象(POJO)

JSON并不仅仅是以String格式出现的--有时,它被存储在一个文件中。例如,JSON可以用来格式化一个配置文件的属性(可以在配置对象中加载,以设置应用程序的状态)。

readValue() 函数可以将文件中的JSON数据直接映射到一个对象中,方法也是接受File

<T> T readValue(File src, Class<T> valueType)

API没有什么变化--你把文件装进去,然后把它传给readValue() 方法。

ObjectMapper objectMapper = new ObjectMapper();
File file = new File("<path-to-file>/HealthWorker.json");
HealthWorker healthWorker = objectMapper.readValue(file, HealthWorker.class);

**注意:**如果你使用一个FileReader 对象而不是File 对象,其效果也是一样的。

从HTTP响应/URL将JSON转换为Java对象(POJO)。

JSON被创建为一种数据交换格式,特别是用于网络应用。同样,它是网络上最普遍的数据序列化格式。虽然你可以检索结果,将其保存为一个字符串,然后使用readValue() 方法进行转换 - 你可以直接读取HTTP响应,给定一个URL,并将其反序列化为所需的类。

<T> T readValue(URL src, Class<T> valueType)

通过这种方法,你可以跳过中间的String,直接解析HTTP请求的结果!

让我们考虑一个天气预报管理系统,我们依靠气象部门的网络服务共享数据。

String API_KEY = "552xxxxxxxxxxxxxxxxx122&";
String URLString = "http://api.weatherapi.com/v1/astronomy.json?key="+API_KEY+"q=London&dt=2021-12-30\n";
URL url = new URL(URLString); // Create a URL object, don't just use a URL as a String
ObjectMapper objectMapper = new ObjectMapper();
Astronomy astronomy = objectMapper.readValue(url, Astronomy.class);

下面是我们的astronomy 对象将包含的一个快照。

Output of the mapping of JSON data from URL to Java Object

同样,Astronomy 类只是反映了预期的JSON结构。

将JSON输入流转换为Java对象(POJO)

InputStream 代表任何任意的字节流,并且不是一种不常见的接收数据的格式。当然,ObjectMapper 也可以读取InputStream ,并将传入的数据映射到一个目标类。

<T> T readValue(InputStream src, Class<T> valueType)

例如,让我们把JSON数据从一个FileInputStream

ObjectMapper objectMapper = new ObjectMapper();
InputStream inputStream = new FileInputStream("<path-to-file>/HealthWorker.json");
HealthWorker healthWorker = objectMapper.readValue(inputStream, HealthWorker.class);

将JSON字节数组转换为Java对象(POJO)

JSON字节数组JSON可以用来存储数据,最常见的是以blob的形式(比如在PostgreSQL或MySQL这样的关系型数据库中)。在另一个运行时,该blob被检索并反序列化为一个对象。BLOB 数据类型是特别重要的,因为它通常被各种应用程序使用,包括消息经纪人,以存储文件的二进制信息。

Flow diagram of Serialization and Deserialization with the Blob data type

ObjectMapper 类的readValue() 方法也可以用来读取字节数组。

<T> T readValue(byte[] src, Class<T> valueType)

如果你有JSON数据作为一个字节数组(byte[]),你就会像通常那样映射它。

ObjectMapper objectMapper = new ObjectMapper();
String healthWorkerJSON = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";
// Ensure UTF-8 format
byte[] jsonByteArray = healthWorkerJSON.getBytes("UTF-8");
HealthWorker healthWorker = objectMapper.readValue(jsonByteArray, HealthWorker.class);

将JSON数组转换为Java对象数组或列表

从JSON数组中读取数据并将其转换为Java对象的数组或列表是另一个用例--你不仅仅是搜索单一资源。它使用的签名与读取单个对象相同。

<T> T readValue(String content, TypeReference<T> valueTypeRef)

只要JSON包含一个数组,我们就可以把它映射成一个对象的数组。

String healthWorkersJsonArray = "[{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5},{\"id\":2,\"name\":\"MichaelJohn\",\"qualification\":\"FCPS\",\"yearsOfExperience\":5}]";
ObjectMapper objectMapper = new ObjectMapper();
HealthWorker[] healthWorkerArray = objectMapper.readValue(healthWorkersJsonArray, HealthWorker[].class);
// OR
HealthWorker[] healthWorkerArray = objectMapper.readValue(jsonKeyValuePair, new TypeReference<HealthWorker[]>(){});

不过,由于数组的工作很混乱--你也可以很容易地将JSON数组转换为一个对象的列表。

String healthWorkersJsonArray = "[{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5},{\"id\":2,\"name\":\"MichaelJohn\",\"qualification\":\"FCPS\",\"yearsOfExperience\":5}]";
ObjectMapper objectMapper = new ObjectMapper();
List<HealthWorker> healthWorkerList = objectMapper.readValue(healthWorkersJsonArray, new TypeReference<List<HealthWorker>(){});

将JSON字符串转换为Java Map

Map 类是用来在Java中存储键值对的。JSON对象是键值对,所以从一个对象映射到另一个对象是很自然的事情

<T> T readValue(String content, TypeReference<T> valueTypeRef)

我们可以把JSON数据转换成Map 对象,JSON的键对应于地图的键,JSON的值对应于地图的值,就像这样简单。

String jsonKeyValuePair = "{\"TeamPolioVaccine\":10,\"TeamMMRVaccine\":19}";
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> jsonMap = objectMapper.readValue(jsonKeyValuePair, new TypeReference<HashMap>(){});
// OR
Map<String, Object> jsonMap = objectMapper.readValue(jsonKeyValuePair, HashMap.class);

这个Map 会包含。

{TeamPolioVaccine=10, TeamMMRVaccine=19}

将Java对象(POJOs)转换成JSON

我们已经看到了许多可以表示JSON数据的方法和输入源,以及如何将这些数据转换成预定义的Java类。现在,让我们反其道而行之,看看如何将Java对象序列化为JSON数据吧

类似于反过来的转换--writeValue() 方法是用来将Java对象序列化为JSON的。

你可以把对象写到字符串、文件或输出流中。

将Java对象转换为JSON字符串

同样,你的对象可以被序列化的最简单形式是一个JSON格式的字符串。

String writeValueAsString(Object value)

另外,更罕见的是,你可以把它写到一个文件中。

void writeValue(File resultFile, Object value)

这里的种类较少,因为大部分的种类都可以在接收端产生。让我们把一个HealthWorker 写成JSON。

ObjectMapper objectMapper = new ObjectMapper();
HealthWorker healthWorker = createHealthWorker();
// Write object into a File
objectMapper.writeValue(new File("healthWorkerJsonOutput.json"),healthWorker);
// Write object into a String
String healthWorkerJSON = objectMapper.writeValueAsString(healthWorker);
System.out.println(healthWorkerJSON);

private static HealthWorker createHealthWorker() {
    HealthWorker healthWorker = new HealthWorker();
    healthWorker.setId(1);
    healthWorker.setName("Dr. John");
    healthWorker.setQualification("FCPS");
    healthWorker.setYearsOfExperience(5.0);
    return healthWorker;
}

healthWorkerJsonOutput.json 是在当前目录下创建的,内容如下。

{
  "id": 1,
  "name": "Dr. John",
  "qualification": "FCPS",
  "yearsOfExperience": 5.0
}

将Java对象转换为FileOutputStream

当把对象保存到JSON文件时--内容在保存前被内部转换为FileOutputStream ,你可以直接使用一个OuputStream

void writeValue(OutputStream out, Object value)

该API的工作方式与之前看到的基本相同。

ObjectMapper objectMapper = new ObjectMapper();
HealthWorker healthWorker = createHealthWorker();
objectMapper.writeValue(new FileOutputStream("output-health-workers.json"), healthWorker);

这将导致一个文件,output-health-workers.json ,包含。

{
  "id": 1,
  "name": "Dr. John",
  "qualification": "FCPS",
  "yearsOfExperience": 5.0
}

Jackson的JSON树模型 - 未知的JSON结构

一个JSON对象也可以用Jackson内置的树形模型而不是预定义的类来表示。当我们不知道接收的JSON会是什么样子,或者我们无法设计一个类来有效地表示它时,Jackson的树模型是很有帮助的。

JsonNode的概述

JsonNode 是所有JSON节点的一个基类,它构成了Jackson的JSON树模型的基础。它驻扎在软件包 。com.fasterxml.jackson.databind.JsonNode

Jackson可以将JSON读入JsonNode 实例,并使用ObjectMapper 类将JSON写到JsonNode 。根据定义,JsonNode 是一个抽象的类,不能直接实例化。然而,有19个JsonNode 的子类,我们可以用来创建对象!

使用ObjectMapper将Java对象转换为JsonNode

ObjectMapper 类提供了两个方法,将数据从Java Object绑定到JSON树。

<T extends JsonNode> T valueToTree(Object fromValue)

以及。

<T> T convertValue(Object fromValue, Class<T> toValueType)

在本指南中,我们将使用valueToTree() 。这类似于将值序列化为JSON,但它更有效率。下面的例子演示了我们如何将一个对象转换为一个JsonNode

ObjectMapper objectMapper = new ObjectMapper();
HealthWorkerService healthWorkerService = new HealthWorkerService();
HealthWorker healthWorker = healthWorkerService.findHealthWorkerById(1);
JsonNode healthWorkerJsonNode = objectMapper.valueToTree(healthWorker);

使用ObjectMapper将JsonNode转换为对象

ObjectMapper 类还提供了两个方便的方法,将数据从JSON树绑定到另一种类型(通常是POJO)。

<T> T treeToValue(TreeNode n, Class<T> valueType)

和。

<T> T convertValue(Object fromValue, Class<T> toValueType)

在本指南中,我们将使用treeToValue() 。下面的代码演示了如何将JSON转换为一个对象,首先将其转换为JsonNode 对象。

String healthWorkerJSON = "{\n\t\"id\": null,\n\t\"name\": \"Reham Muzzamil\",\n\t\"qualification\": \"MBBS\",\n\t\"yearsOfExperience\": 1.5\n}";
ObjectMapper objectMapper = new ObjectMapper();

JsonNode healthWorkerJsonNode = objectMapper.readTree(healthWorkerJSON);
HealthWorker healthWorker = objectMapper.treeToValue(healthWorkerJsonNode, HealthWorker.class);

配置ObjectMapper的序列化和反序列化

通过Jackson API的默认反序列化技术,输入的JSON可能与目标POJO不同或不兼容。这里有几个例子。

  • 一个JSON字符串的字段在相关的POJO中是不可用的。
  • 在一个JSON字符串中,原始类型的字段有空值。

这两种情况都很常见,而且你一般都希望能够处理它们。值得庆幸的是,这两种情况都很容易恢复。还有一些情况,我们希望在整个序列化过程中管理定制,比如说

  • 使用文本格式来序列化Date 对象而不是时间戳。
  • 当没有找到某个特定类型的访问器时,控制序列化过程的行为。

在这些情况下,我们可以配置ObjectMapper 对象来改变其行为。configure() 方法允许我们改变默认的序列化和反序列化方法。

ObjectMapper configure(SerializationFeature f, boolean state)
ObjectMapper configure(DeserializationFeature f, boolean state)

这里有一个广泛的属性列表,我们将看一下更相关的属性。它们都有合理的默认值--在大多数情况下你不必改变它们,但在更特殊的情况下,知道你可以改变哪些属性是非常有用的。

fail_on_empty_beans

FAIL_ON_EMPTY_BEANS 序列化功能定义了当没有找到一个类型的访问器(属性)时会发生什么。如果启用(默认),就会抛出一个异常,表明Bean是不可序列化的。如果禁用,Bean将被序列化为一个没有属性的空Object。

我们希望在一些情况下禁用该功能,比如当一个类只有配置相关的导入而没有属性字段时,但在某些情况下,如果你在处理一个没有公共方法/属性的对象时,这个异常可能会 "绊倒 "你,导致一个不需要的异常。

让我们考虑一个空的Java类。

class SoftwareEngineer {}

ObjectMapper 类在试图序列化一个没有属性的类时,会抛出以下异常。

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.stackabuse.tutorial.SoftwareEngineer and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

在这种情况下,禁用该功能有助于顺利地处理序列化。下面的代码片断演示了如何禁用这个序列化属性。

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
System.out.println(objectMapper.writeValueAsString(new SoftwareEngineer()));

执行上述代码片断的结果是一个空的对象。

{}

写入_dates_as_timestamps

日期可以用无数种格式来写,而且不同国家的日期格式也不同。WRITE_DATES_AS_TIMESTAMPS 功能定义了你是想把日期字段写成数字时间戳还是其他类型。

默认情况下,该功能被设置为true ,因为这是一种非常普遍的表示日期的方式--而且前面提到的无数格式可以比其他格式更容易从时间戳中得到。另外,你可能想强制使用一种更方便用户的格式。

Date date = Calendar.getInstance().getTime();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String dateString = dateFormat.format(date);
System.out.println(dateString);

ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(date));
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
System.out.println(objectMapper.writeValueAsString(date));

运行上面的代码会给我们这样的输出。

2022-01-01 08:34:55
1641051295217
"2022-01-01T15:34:55.217+00:00"

fail_on_unknown_properties

如果JSON字符串包含POJO不熟悉的字段,无论是单一的String ,还是更多的字段,反序列化过程会抛出一个 UnrecognizedPropertyException.如果我们不关心捕捉每一个数据字段怎么办?

当与第三方API一起工作时,你可以预期JSON响应会随着时间的推移而改变。最常见的是,这些变化并没有公布,所以一个新的属性可能会悄悄出现,这将破坏你的代码修复方法很简单--只需将新属性添加到你的POJO中。但在某些情况下,这将需要更新其他的类、DTO、资源类等,只是因为第三方添加了一个可能与你无关的属性。

这就是为什么,FAIL_ON_UNKNOWN_PROPERTIES 默认设置为false ,如果有新的属性,Jackson会直接忽略这些属性。

另一方面,你可能想在一个项目中强制响应团结--使API之间传输的数据标准化,而不是在属性(错误地)改变时Jackson默默地忽略它们。这将 "提醒 "你正在进行的任何改变。

ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
String healthWorkerJsonUpdated = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5,\"specialization\":\"Peadiatrics\"}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJsonUpdated, HealthWorker.class);

上面的代码在JSON字符串中引入了一个未知的属性specialization 。运行它将导致以下异常。

Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "specialization" (class com.stackabuse.model.HealthWorker), not marked as ignorable (4 known properties: "id", "qualification", "name", "yearsOfExperience"])

注意:将这个属性设置为true ,会影响所有由ObjectMapper 实例创建的POJO。为了避免这种更 "全局 "的配置,我们可以在类的层面上添加这个注解:@JsonIgnoreProperties(ignoreUnknown = true)

fail_on_null_for_primitives

FAIL_ON_NULL_FOR_PRIMITIVES 功能决定了当遇到JSON属性作为null ,而反序列化为Java原始类型(如intdouble )时是否会失败。默认情况下,原始字段的空值被忽略。然而,我们可以配置ObjectMapper ,在这些字段的遗漏预示着一个更大的错误的情况下,反而会失败。

下面的代码启用了这个反序列化功能。

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true);
String healthWorkerJSON = "{\"id\":null,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON, HealthWorker.class);

这将导致。

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot map `null` into type `int` (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)

accept_empty_string_as_null_object

当我们想允许或不允许JSON空字符串值"" 绑定到POJO作为null ,我们可以配置这个属性。默认情况下,这个功能是打开的

为了演示这个反序列化功能的使用,我们对我们的HealthWorker 类做了如下修改。

public class HealthWorker {

    private int id;
    private String name;
    private String qualification;
    private Double yearsOfExperience;
    private Specialization specialization;

    // Constructor, getters, setters, toString()
}

它现在有一个名为specialization 的属性,它的定义如下。

public class Specialization {
    private String specializationField;

    // Constructor, getters, setters, toString()
}

让我们把一些输入的JSON映射到一个HealthWorker 对象。

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
String healthWorkerJSON = "{\"id\":1,\"name\":\"\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5,\"specialization\":\"\"}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON, HealthWorker.class);
System.out.println(healthWorker.getSpecialization());

这就导致了。

null

用Jackson创建一个自定义序列化器和反序列化器

早些时候,我们遇到了JSON字符串字段和Java对象字段之间的不匹配,它们很容易通过注解相互 "适应"。然而,有时候,这种不匹配是结构上的,而不是语义上的。

ObjectMapper 类允许你为这些情况注册一个自定义的序列器或反序列器。当JSON结构与需要序列化或反序列化的Java POJO类不同时,这个功能很有帮助。

为什么呢?嗯,你可能想使用JSON中的数据或类作为不同的类型。例如,一个API可能提供一个数字,但在你的代码中,你想把它作为一个字符串使用。

在我们能够轻松定制序列化器和反序列化器之前,开发者通常会使用数据传输对象(DTO)--与API交互的类,然后用来填充我们的POJO。

DTO conversion in early era

如果你想了解更多关于DTOs的信息--请阅读我们的《Java中的数据传输对象模式指南--实现和映射》!

自定义序列化器使我们能够跳过这一步。让我们深入了解一下吧

实现一个自定义的Jackson序列化器

让我们实现几个序列化器来感受一下它们的使用方法。这个序列化器接收一个本地的DateTime 值,并将其格式化为一个读者/API友好的字符串。

public class CustomJodaDateTimeSerializer extends StdSerializer<DateTime> {

    private static DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");

    public CustomJodaDateTimeSerializer() {
        this(null);
    }

    public CustomJodaDateTimeSerializer(Class<DateTime> t) {
        super(t);
    }

    @Override
    public void serialize(DateTime value, JsonGenerator jsonGenerator, SerializerProvider arg2) throws IOException {
        jsonGenerator.writeString(formatter.print(value));
    }
}

这个序列化器将一个双倍值(例如,美元和美分的价格)转换为字符串。

public class DoubleToStringCustomSerializer extends JsonSerializer<Double> {

    @Override
    public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(value.toString());
    }
}

这个序列化器基于一个HealthWorker 对象的数据返回一个JSON对象。注意从Java对象的name 属性和JSON的full_name 的变化。

public class HealthWorkerCustomSerializer extends StdSerializer<HealthWorker> {

    private static final long serialVersionUID = 1L;

    public HealthWorkerCustomSerializer() {
        this(null);
    }

    public HealthWorkerCustomSerializer(Class clazz) {
        super(clazz);
    }

    @Override
    public void serialize(HealthWorker healthWorker, JsonGenerator jsonGenerator, SerializerProvider serializer)
    throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeNumberField("id", healthWorker.getId());
        jsonGenerator.writeStringField("full_name",
        healthWorker.getName());
        jsonGenerator.writeStringField("qualification", healthWorker.getQualification());
        jsonGenerator.writeObjectField("yearsOfExperience", healthWorker.getYearsOfExperience());
        jsonGenerator.writePOJOField("dateOfJoining", healthWorker.getDateOfJoining());
        jsonGenerator.writeEndObject();
    }
}

让我们假设我们可以用一个HealthWorkerService 对象来检索医疗卫生工作者的数据,这样就可以利用网络服务来按ID查找医疗卫生工作者。这就是如何设置像我们上面创建的自定义序列化器。

ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();

simpleModule.addSerializer(DateTime.class, new CustomJodaDateTimeSerializer());
simpleModule.addSerializer(Double.class, new DoubleToStringCustomSerializer());
simpleModule.addSerializer(HealthWorker.class, new HealthWorkerCustomSerializer());
objectMapper.registerModule(simpleModule);

HealthWorkerService healthWorkerService = new HealthWorkerService();
HealthWorker healthWorker = healthWorkerService.findHealthWorkerById(1);
String healthWorkerCustomSerializedJson = objectMapper.writeValueAsString(healthWorker);
System.out.println(healthWorkerCustomSerializedJson);

观察一下序列化器是如何被添加到一个模块中的,然后由ObjectMapper

{
  "id": 1,
  "full_name": "Dr. John",
  "qualification": "FCPS",
  "yearsOfExperience": "5.0",
  "dateOfJoining": "2022-01-02 00:28"
}

在这里,我们可以观察到name 字段被改变为full_nameyearsOfExperience 的值被返回为"5.0" ,这是一个字符串值,而dateOfJoining 的值被按照定义的格式返回。

实现自定义杰克逊反序列化器

下面是一个自定义反序列化器的实现,它将一个值附加到name

public class HealthWorkerCustomDeserializer extends StdDeserializer {

    private static final long serialVersionUID = 1L;

    public HealthWorkerCustomDeserializer() {
        this(null);
    }

    public HealthWorkerCustomDeserializer(Class clazz) {
        super(clazz);
    }

    @Override
    public HealthWorker deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        HealthWorker healthWorker = new HealthWorker();
        JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
        JsonNode customNameNode = jsonNode.get("name");
        JsonNode customQualificationNode = jsonNode.get("qualification");
        JsonNode customYearsOfExperienceNode = jsonNode.get("yearsOfExperience");
        JsonNode customIdNode = jsonNode.get("yearsOfExperience");
        String name = "Dr. " + customNameNode.asText();
        String qualification = customQualificationNode.asText();
        Double experience = customYearsOfExperienceNode.asDouble();
        int id = customIdNode.asInt();
        healthWorker.setName(name);
        healthWorker.setQualification(qualification);
        healthWorker.setYearsOfExperience(experience);
        healthWorker.setId(id);
        return healthWorker;
    }
}

添加一个反序列化器与添加一个序列化器相似,它们被添加到模块中,然后被注册到ObjectMapper 实例中。

ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(HealthWorker.class, new HealthWorkerCustomDeserializer());
objectMapper.registerModule(simpleModule);
String healthWorkerJSON = "{\n\t\"id\": 1,\n\t\"name\": \"Reham Muzzamil\",\n\t\"qualification\": \"MBBS\",\n\t\"yearsOfExperience\": 1.5\n}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON,HealthWorker.class);
System.out.println(healthWorker.getName());

运行这段代码将产生这个输出。

Dr. Reham Muzzamil

从输出中我们可以看到,Dr. ,按照自定义的反序列化逻辑,被附加到健康工作者的名字上。

总结

这就给我们带来了本指南的结论。我们已经介绍了ObjectMapper类--Jackson用于Java对象和JSON数据的序列化和反序列化的中心API。

我们首先看了一下如何安装Jackson,然后深入探讨了将JSON转换为Java对象--从字符串、文件、HTTP响应、InputStreams和字节数组。然后我们探讨了JSON到Java列表和地图的转换。

在将Java对象转换为JSON数据之前,我们已经介绍了@JsonProperty@JsonAlias 注释来 "桥接 "不匹配的字段名。

当你不知道传入的JSON的结构时--你可以使用通用的JsonNode 类来保存结果!

随着一般用法的结束,我们已经探索了一些自定义标记,这些标记修改了ObjectMapper的行为,甚至实现了我们自己的几个序列化器和反序列化器。