Jackson 解析 JSON 詳細教學

2022-07-22 12:06:02

點贊再看,動力無限。 微信搜「 程式猿阿朗 」。

本文 Github.com/niumoo/JavaNotes未讀程式碼部落格 已經收錄,有很多知識點和系列文章。

JSON 對於開發者並不陌生,如今的 WEB 服務、移動應用、甚至物聯網大多都是以 JSON 作為資料交換的格式。學習 JSON 格式的操作工具對開發者來說是必不可少的。這篇文章將介紹如何使用 Jackson 開源工具庫對 JSON 進行常見操作。

JSON 介紹

什麼是 JSON ?JSON 是 」JavaScript Object Notation「 的縮寫,JSON 是一種基於文字的格式,可以把它理解為是一個結構化的資料,這個結構化資料中可以包含鍵值對映、巢狀物件以及陣列等資訊。

{
  "array": [
    1,
    2,
    3
  ],
  "boolean": true,
  "color": "gold",
  "null": null,
  "number": 123,
  "object": {
    "a": "b",
    "c": "d"
  },
  "string": "www.wdbyte.com"
}

Jackson 介紹

Jackson 和 FastJson 一樣,是一個 Java 語言編寫的,可以進行 JSON 處理的開源工具庫,Jackson 的使用非常廣泛,Spring 框架預設使用 Jackson 進行 JSON 處理。

Jackson 有三個核包,分別是 Streaming、Databid、Annotations,通過這些包可以方便的對 JSON 進行操作。

  • Streamingjackson-core 模組。 定義了一些流處理相關的 API 以及特定的 JSON 實現。
  • Annotationsjackson-annotations 模組,包含了 Jackson 中的註解。
  • Databindjackson-databind 模組, 在 Streaming 包的基礎上實現了資料繫結,依賴於 StreamingAnnotations 包。

得益於 Jackson 高擴充套件性的設計,有很多常見的文字格式以及工具都有對 Jackson 的相應適配,如 CSV、XML、YAML 等。

Jackson Maven 依賴

在使用 Jackson 時,大多數情況下我們只需要新增 jackson-databind 依賴項,就可以使用 Jackson 功能了,它依賴了下面兩個包。

  • com.fasterxml.jackson.core:jackson-annotations
  • com.fasterxml.jackson.core:jackson-core
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.3</version>
</dependency>

為了方便這篇文章後續的程式碼演示,我們同時引入 Junit 進行單元測試和 Lombok 以減少 Get/Set 的程式碼編寫。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.8.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
</dependency>

ObjectMapper 物件對映器

ObjectMapper 是 Jackson 庫中最常用的一個類,使用它可以進行 Java 物件和 JSON 字串之間快速轉換。如果你用過 FastJson,那麼 Jackson 中的 ObjectMapper 就如同 FastJson 中的 JSON 類。

這個類中有一些常用的方法:

  • readValue() 方法可以進行 JSON 的反序列化操作,比如可以將字串、檔案流、位元組流、位元組陣列等將常見的內容轉換成 Java 物件。
  • writeValue() 方法可以進行 JSON 的序列化操作,可以將 Java 物件轉換成 JSON 字串。

大多數情況下,ObjectMapper 的工作原理是通過 Java Bean 物件的 Get/Set 方法進行轉換時對映的,所以正確編寫 Java 物件的 Get/Set 方法尤為重要,不過 ObjectMapper 也提供了諸多設定,比如可以通過設定或者註解的形式對 Java 物件和 JSON 字串之間的轉換過程進行自定義。這些在下面部分都會介紹到。

Jackson JSON 基本操作

Jackson 作為一個 Java 中的 JSON 工具庫,處理 JSON 字串和 Java 物件是它最基本最常用的功能,下面通過一些例子來演示其中的用法。

Jackson JSON 序列化

編寫一個 Person 類,定義三個屬性,名稱、年齡以及技能。

/**
 * @author https://www.wdbyte.com
 */
@Data
public class Person {
    private String name;
    private Integer age;
    private List<String> skillList;
}

將 Java 物件轉換成 JSON 字串。

import java.util.Arrays;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * @author https://www.wdbyte.com
 */
class PersonTest {

    ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void pojoToJsonString() throws JsonProcessingException {
        Person person = new Person();
        person.setName("aLng");
        person.setAge(27);
        person.setSkillList(Arrays.asList("java", "c++"));

        String json = objectMapper.writeValueAsString(person);
        System.out.println(json);
        String expectedJson = "{\"name\":\"aLng\",\"age\":27,\"skillList\":[\"java\",\"c++\"]}";
        Assertions.assertEquals(json, expectedJson);
    }
}

輸出的 JSON 字串:

{"name":"aLng","age":27,"skillList":["java","c++"]}

Jackson 甚至可以直接把序列化後的 JSON 字串寫入檔案或者讀取成位元組陣列。

mapper.writeValue(new File("result.json"), myResultObject);
// 或者
byte[] jsonBytes = mapper.writeValueAsBytes(myResultObject);
// 或者
String jsonString = mapper.writeValueAsString(myResultObject);

Jackson JSON 反序列化

直接貼出程式碼:

package com.wdbyte.jackson;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * @author https://www.wdbyte.com
 */
class PersonTest {

    ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void jsonStringToPojo() throws JsonProcessingException {
        String expectedJson = "{\"name\":\"aLang\",\"age\":27,\"skillList\":[\"java\",\"c++\"]}";
        Person person = objectMapper.readValue(expectedJson, Person.class);
        System.out.println(person);
        Assertions.assertEquals(person.getName(), "aLang");
        Assertions.assertEquals(person.getSkillList().toString(), "[java, c++]");
    }
}

輸出結果:

Person(name=aLang, age=27, skillList=[java, c++])

上面的例子演示瞭如何使用 Jackson 把一個 JSON 字串反序列化成 Java 物件,其實 Jackson 對檔案中的 JSON 字串、位元組形式的 JSON 字串反序列化同樣簡單。

比如先準備了一個 JSON 內容檔案 Person.json。

{
  "name": "aLang",
  "age": 27,
  "skillList": [
    "java",
    "c++"
  ]
}

下面進行讀取轉換。

ObjectMapper objectMapper = new ObjectMapper();

@Test
void testJsonFilePojo() throws IOException {
    File file = new File("src/Person.json");
    Person person = objectMapper.readValue(file, Person.class);
  	// 或者
    // person = mapper.readValue(new URL("http://some.com/api/entry.json"), MyValue.class);
    System.out.println(person);
    Assertions.assertEquals(person.getName(), "aLang");
    Assertions.assertEquals(person.getSkillList().toString(), "[java, c++]");
}

同樣輸出了 Person 內容。

Person(name=aLang, age=27, skillList=[java, c++])

JSON 轉 List

上面演示 JSON 字串都是單個物件的,如果 JSON 是一個物件列表那麼使用 Jackson 該怎麼處理呢?

已經存在一個檔案 PersonList.json.

[
  {
    "name": "aLang",
    "age": 27,
    "skillList": [
      "java",
      "c++"
    ]
  },
  {
    "name": "darcy",
    "age": 26,
    "skillList": [
      "go",
      "rust"
    ]
  }
]

讀取它然後轉換成 List<Person>

ObjectMapper objectMapper = new ObjectMapper();

@Test
void fileToPojoList() throws IOException {
    File file = new File("src/EmployeeList.json");
    List<Person> personList = objectMapper.readValue(file, new TypeReference<List<Person>>() {});
    for (Person person : personList) {
        System.out.println(person);
    }
    Assertions.assertEquals(personList.size(), 2);
    Assertions.assertEquals(personList.get(0).getName(), "aLang");
    Assertions.assertEquals(personList.get(1).getName(), "darcy");
}

可以輸出物件內容:

Person(name=aLang, age=27, skillList=[java, c++])
Person(name=darcy, age=26, skillList=[go, rust])

JSON 轉 Map

JSON 轉 Map 在我們沒有一個物件的 Java 物件時十分實用,下面演示如何使用 Jackson 把 JSON 文字轉成 Map 物件。

ObjectMapper objectMapper = new ObjectMapper();

@Test
void jsonStringToMap() throws IOException {
    String expectedJson = "{\"name\":\"aLang\",\"age\":27,\"skillList\":[\"java\",\"c++\"]}";
    Map<String, Object> employeeMap = objectMapper.readValue(expectedJson, new TypeReference<Map>() {});
    System.out.println(employeeMap.getClass());
    for (Entry<String, Object> entry : employeeMap.entrySet()) {
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
    Assertions.assertEquals(employeeMap.get("name"), "aLang");
}

可以看到 Map 的輸出結果:

class java.util.LinkedHashMap
name:aLang
age:27
skillList:[java, c++]

Jackson 忽略欄位

如果在進行 JSON 轉 Java 物件時,JSON 中出現了 Java 類中不存在的屬性,那麼在轉換時會遇到 com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException 異常。

使用 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 可以忽略不存在的屬性。

ObjectMapper objectMapper = new ObjectMapper();

@Test
void jsonStringToPojoIgnoreProperties() throws IOException {
    // UnrecognizedPropertyException
    String json = "{\"yyy\":\"xxx\",\"name\":\"aLang\",\"age\":27,\"skillList\":[\"java\",\"c++\"]}";
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    Person person = objectMapper.readValue(json, Person.class);
  	System.out.printf(person.toString());
    Assertions.assertEquals(person.getName(), "aLang");
    Assertions.assertEquals(person.getSkillList().toString(), "[java, c++]");
}

正常輸出:

Person(name=aLang, age=27, skillList=[java, c++])

Jackson 日期格式化

在 Java 8 之前我們通常使用 java.util.Date 類來處理時間,但是在 Java 8 釋出時引入了新的時間類 java.time.LocalDateTime. 這兩者在 Jackson 中的處理略有不同。

先建立一個有兩種時間型別屬性的 Order 類。

package com.wdbyte.jackson;

import java.time.LocalDateTime;
import java.util.Date;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author https://www.wdbyte.com
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
  
    private Integer id;

    private Date createTime;

    private LocalDateTime updateTime;
}

Date 型別

下面我們新建一個測試用例來測試兩種時間型別的 JSON 轉換。

package com.wdbyte.jackson;

import java.time.LocalDateTime;
import java.util.Date;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * @author https://www.wdbyte.com
 */
class OrderTest {

    ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void testPojoToJson0() throws JsonProcessingException {
        Order order = new Order(1, new Date(), null);
        String json = objectMapper.writeValueAsString(order);
        System.out.println(json);

        order = objectMapper.readValue(json, Order.class);
        System.out.println(order.toString());

        Assertions.assertEquals(order.getId(), 1);
    }

}

在這個測試程式碼中,我們只初始化了 Date 型別的時間,下面是輸出的結果:

{"id":1,"createTime":1658320852395,"updateTime":null}
Order(id=1, createTime=Wed Jul 20 20:40:52 CST 2022, updateTime=null)

可以看到正常的進行了 JSON 的序列化與反序列化,但是 JSON 中的時間是一個時間戳格式,可能不是我們想要的。

LocalDateTime 型別

為什麼沒有設定 LocalDateTime 型別的時間呢?因為預設情況下進行 LocalDateTime 類的 JSON 轉換會遇到報錯。

/**
 * @author https://www.wdbyte.com
 */
class OrderTest {

    ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void testPojoToJson() throws JsonProcessingException {
        Order order = new Order(1, new Date(), LocalDateTime.now());
        String json = objectMapper.writeValueAsString(order);
        System.out.println(json);

        order = objectMapper.readValue(json, Order.class);
        System.out.println(order.toString());

        Assertions.assertEquals(order.getId(), 1);
    }
}

執行後會遇到報錯:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
			Java 8 date/time type `java.time.LocalDateTime` not supported by default: 
				add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" 
          to enable handling (through reference chain: com.wdbyte.jackson.Order["updateTime"])

這裡我們需要新增相應的資料繫結支援包。

新增依賴:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.13.3</version>
</dependency>

然後在定義 ObjectMapper 時通過 findAndRegisterModules() 方法來註冊依賴。

import java.time.LocalDateTime;
import java.util.Date;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * @author https://www.wdbyte.com
 */
class OrderTest {

    ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();

    @Test
    void testPojoToJson() throws JsonProcessingException {
        Order order = new Order(1, new Date(), LocalDateTime.now());
        String json = objectMapper.writeValueAsString(order);
        System.out.println(json);

        order = objectMapper.readValue(json, Order.class);
        System.out.println(order.toString());

        Assertions.assertEquals(order.getId(), 1);
    }
}

執行可以得到正常序列化與反序列化紀錄檔,不過序列化後的時間格式依舊奇怪。

{"id":1,"createTime":1658321191562,"updateTime":[2022,7,20,20,46,31,567000000]}
Order(id=1, createTime=Wed Jul 20 20:46:31 CST 2022, updateTime=2022-07-20T20:46:31.567)

時間格式化

通過在欄位上使用註解 @JsonFormat 來自定義時間格式。

import java.time.LocalDateTime;
import java.util.Date;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {

    private Integer id;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private Date createTime;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private LocalDateTime updateTime;
}

再次執行上面的列子可以得到時間格式化後的 JSON 字串。

{"id":1,"createTime":"2022-07-20 20:49:46","updateTime":"2022-07-20 20:49:46"}
Order(id=1, createTime=Wed Jul 20 20:49:46 CST 2022, updateTime=2022-07-20T20:49:46)

Jackson 常用註解

@JsonIgnore

使用 @JsonIgnore 可以忽略某個 Java 物件中的屬性,它將不參與 JSON 的序列化與反序列化。

範例:

package com.wdbyte.jackson;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;

/**
 * @author https://www.wdbyte.com
 */
@Data
public class Cat {

    private String name;

    @JsonIgnore
    private Integer age;
}

編寫單元測試類。

package com.wdbyte.jackson;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * @author https://www.wdbyte.com
 */
class CatTest {

    ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void testPojoToJson() throws JsonProcessingException {
        Cat cat = new Cat();
        cat.setName("Tom");
        cat.setAge(2);
        String json = objectMapper.writeValueAsString(cat);
        System.out.println(json);

        Assertions.assertEquals(json, "{\"name\":\"Tom\"}");

        cat = objectMapper.readValue(json, Cat.class);
        Assertions.assertEquals(cat.getName(), "Tom");
        Assertions.assertEquals(cat.getAge(), null);
    }

}

輸出結果中 age 屬性為 null

{"name":"Tom"}

@JsonGetter

使用 @JsonGetter 可以在對 Java 物件進行 JSON 序列化時自定義屬性名稱。

import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;

/**
 * @author https://www.wdbyte.com
 */
@Data
public class Cat {

    private String name;

    private Integer age;

    @JsonGetter(value = "catName")
    public String getName() {
        return name;
    }
}

編寫單元測試類進行測試。

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * @author https://www.wdbyte.com
 */
class CatTest {

    ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void testPojoToJson2() throws JsonProcessingException {
        Cat cat = new Cat();
        cat.setName("Tom");
        cat.setAge(2);
        String json = objectMapper.writeValueAsString(cat);
        System.out.println(json);
        Assertions.assertEquals(json, "{\"age\":2,\"catName\":\"Tom\"}");
    }
}

輸出結果,name 已經設定成了 catName

{"age":2,"catName":"Tom"}

@JsonSetter

使用 @JsonSetter 可以在對 JSON 進行反序列化時設定 JSON 中的 key 與 Java 屬性的對映關係。

package com.wdbyte.jackson;

import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;
import lombok.Data;

/**
 * @author https://www.wdbyte.com
 * @date 2022/07/17
 */
@Data
public class Cat {
    @JsonSetter(value = "catName")
    private String name;

    private Integer age;

    @JsonGetter(value = "catName")
    public String getName() {
        return name;
    }
}

編寫單元測試類進行測試。

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * @author https://www.wdbyte.com
 */
class CatTest {

    ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void testPojoToJson2() throws JsonProcessingException {
        String json = "{\"age\":2,\"catName\":\"Tom\"}";
        Cat cat = objectMapper.readValue(json, Cat.class);
        System.out.println(cat.toString());
        Assertions.assertEquals(cat.getName(), "Tom");
    }
}

輸出結果:

Cat(name=Tom, age=2)

@JsonAnySetter

使用 @JsonAnySetter 可以在對 JSON 進行反序列化時,對所有在 Java 物件中不存在的屬性進行邏輯處理,下面的程式碼演示把不存在的屬性存放到一個 Map 集合中。

import java.util.Map;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.google.common.collect.Maps;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author https://www.wdbyte.com
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private String name;
    private Integer age;
    private Map<String, Object> diyMap = new HashMap<>();

    @JsonAnySetter
    public void otherField(String key, String value) {
        this.diyMap.put(key, value);
    }
}

編寫單元測試用例。

import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * @author https://www.wdbyte.com
 */
class StudentTest {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void testJsonToPojo() throws JsonProcessingException {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "aLang");
        map.put("age", 18);
        map.put("skill", "java");

        String json = objectMapper.writeValueAsString(map);
        System.out.println(json);

        Student student = objectMapper.readValue(json, Student.class);
        System.out.println(student);

        Assertions.assertEquals(student.getDiyMap().get("skill"), "java");
    }

}

輸出結果中可以看到 JSON 中的 skill 屬性因為不在 Java 類 Student 中,所以被放到了 diyMap 集合。

{"skill":"java","name":"aLang","age":18}
Student(name=aLang, age=18, diyMap={skill=java})

@JsonAnyGetter

使用 @JsonAnyGetter 可以在對 Java 物件進行序列化時,使其中的 Map 集合作為 JSON 中屬性的來源。下面做個範例。

import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.google.common.collect.Maps;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

/**
 * @author https://www.wdbyte.com
 */
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    @Getter
    @Setter
    private String name;

    @Getter
    @Setter
    private Integer age;
  
    @JsonAnyGetter
    private Map<String, Object> initMap = new HashMap() {{
        put("a", 111);
        put("b", 222);
        put("c", 333);
    }};
}

編寫單元測試用例。

class StudentTest {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void testPojoToJsonTest() throws JsonProcessingException {
        Student student = new Student();
        student.setName("aLang");
        student.setAge(20);
        String json = objectMapper.writeValueAsString(student);
        System.out.println(json);
      
       Assertions.assertEquals(json,"{\"name\":\"aLang\",\"age\":20,\"a\":111,\"b\":222,\"c\":333}");
    }

}

輸出結果:

{"name":"aLang","age":20,"a":111,"b":222,"c":333}

Jackson 總結

  • Jackson 是 Java 中比較流量的 JSON 處理庫之一,它是 Spring 的預設 JSON 工具。

  • Jackson 主要有三個模組組成,Streaming API 、Annotations 和 Data Binding 。

  • Jackson 中的 ObjectMapper 類十分強大,可以進行 JSON 相關處理,同時可以結合註釋以及設定進行自定義轉換邏輯。

  • Jackson 擴充套件性很好,如 CSV、XML、YAML 格式處理都對 Jackson 有相應的適配等。

一如既往,文章程式碼都存放在 Github.com/niumoo/javaNotes.

<完>

文章持續更新,可以微信搜一搜「 程式猿阿朗 」或存取「程式猿阿朗部落格 」第一時間閱讀。本文 Github.com/niumoo/JavaNotes 已經收錄,有很多知識點和系列文章,歡迎Star。