Jackson 使用指南

2024-08-20, 星期二, 16:24

JavaDev

Jackson 的核心由三个模块组成

  • Streaming(jackson-core)提供流式解析工具 JsonParser,流式生成工具 JsonGenerator
  • Annotations(jackson-annotations)提供注解
  • Databind (jackson-databind) 提供 ObjectMapper 用于序列化和反序列化,依赖前两个模块

推荐理由

  • 非常 强大灵活的注解和 API
  • 通过 Module 模块扩展支持 XML、YAML、Properties 的解析与生成
  • org.springframework.boot:spring-boot-starter-web 默认集成 Jackson,基于 Spring Boot 的 Web 项目可直接使用

基础序列化与反序列化

Jackson 使用 com.fasterxml.jackson.databind.ObjectMapper 实现序列化和反序列化操作。在 Spring 项目中可以使用依赖注入获得 mapper,如果你有自己的配置需求,可以自行创建 Bean 在需要的地方注入。

// 序列化
mapper.writeValueAsString(user);
mapper.writeValueAsString(userList);

// 反序列化为对象
Foo foo = mapper.readValue(objectStr, Foo.class);
// 反序列化为数组
Foo[] foos = mapper.readValue(arrayStr, Foo[].class);
// 反序列化为基本类型的 List
List<Integer> list = mapper.readValue(arrayStr, List.class);

// 反序列化为复杂类型的 List
List<Foo> list = mapper.readValue(arrayStr,
                                  new TypeReference<List<Foo>>() {
                                  });

// 反序列化为复杂类型的 Map
Map<String, Foo> map = mapper.readValue(objectStr,
                                        new TypeReference<Map<String, Foo>>() {
                                        });

处理泛型时要注意 Java 的类型擦除行为,不要编写这样的代码:

private <T> Wrapper<T> parse() throws IOException {
        // do something...
        return mapper.readValue(jsonStr,
                                new TypeReference<Wrapper<T>>() { });
}

在方法设计上传递 com.fasterxml.jackson.core.type.TypeReference 类型的参数:

private <T> Wrapper<T> parseWithTypeReference(
    TypeReference<Wrapper<T>> typeReference) throws IOException {
    // do something...
    return mapper.readValue(jsonStr, typeReference);

用 TreeModel 处理 JSONObject 和 JSONArray

你不应该直接处理 JSONObject 和 JSONArray,除非业务逻辑只需要一个庞大 JSON 中的小部分字段,或者 JSON 的结构需要在运行时推测。此时可借助 TreeModel 将 JSON 转换为 com.fasterxml.jackson.databind.JsonNode

String carString = """
{
    "color": "Black",
    "type": "FIAT",
    "specifications": {
        "engine": "1.2L",
        "horsepower": 69,
        "transmission": "Manual"
    }
}
""";

JsonNode node = objectMapper.readTree(carString);
assertEquals("Black",
             node.get("color").asText());

com.fasterxml.jackson.databind.JsonNode#path(java.lang.String) 方法可按指定路径取值:

assertEquals(69,
             node.path("specifications").path("horsepower").asInt());

由于值 / 对象 / 数组都是某种 JsonNode,在不知道 JSON Schema 的情况下分析结构,可以使用 isValueNodeisArrayisObject 等方法辅助判断:

if (column.isArray()) {
	for (final JsonNode field : column) {
		JsonNode prop = field.get("prop");
		if (prop.isValueNode()) {
			logger.info("field {} has loaded.", prop.asText());
		}
	}
}

在 Java 中直接组装 JSONObject 和 JSONArray 也需要借助 TreeModel:

JsonNode node = mapper.valueToTree(fromValue);
// or
JsonNode node = mapper.convertValue(fromValue, JsonNode.class);

// 使用 ObjectMapper
ObjectNode node = mapper.createObjectNode();
ArrayNode nodes = mapper.createArrayNode();

枚举类型

假设你手头有这样一个枚举:

public enum FruitEnum {
    BANANA(0, "banana"),
    APPLE(1, "apple");

    final int code;
    final String name;

    // getters / setters / ...
}

处理和产生 { "fruit": "BANANA" } 不需要做任何修改:

String jsonStr = "{\"fruit\":\"BANANA\"}";
Basket basket = objectMapper.readValue(jsonStr, Basket.class);
assertEquals(FruitEnum.BANANA, basket.getFruit());
assertEquals(jsonStr, objectMapper.writeValueAsString(basket));

如果你希望在 JSON 中使用 code 属性:

public enum FruitEnum {
    BANANA(0, "banana"),
    APPLE(1, "apple");
    // ...

    @JsonValue
    public int getCode() {
        return code;
    }

    @JsonCreator
    public static FruitEnum getFruit(int code) {
        return Stream.of(FruitEnum.values())
            .filter(p -> p.getCode() == code)
            .findFirst()
            .orElseThrow(IllegalArgumentException::new);
    }
}

@JsonValue 告诉 Jackson 在序列化时使用这个方法的返回值作为 JSON 属性的值。Jackson 在反序列化创建对象时会选择无参构造函数或 @JsonCreator 修饰的构造函数。

String jsonStr = "{\"fruit\":0}";
Basket basket = objectMapper.readValue(jsonStr, Basket.class);
assertEquals(FruitEnum.BANANA, basket.getFruit());
assertEquals(jsonStr, objectMapper.writeValueAsString(basket));

ObjectMapper 常用配置

尽管 new ObjectMapper() 获得的 ObjectMapper 已经足够开箱即用,仍然可以对其做一些个性化配置。Spring Boot 项目支持在 properties 文件中使用 spring.jackson 修改 ObjectMapper 的部分行为。

你也可以在代码中初始化自己的 ObjectMapper 实例,使用 configure(SerializationFeature f, boolean state)enable/disable(SerializationFeature f) 配置,并通过单例模式透出,或注册为 Bean。

常用序列化配置

// 空对象不抛出异常
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);

常用反序列化配置

// 允许 JSON 中存在 Bean 未定义的属性
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
// 允许基本类型字段为空
mapper.disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
// 空字符串按 null 处理
mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
// 允许枚举序列化为数字
mapper.disable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)
// 声明为容器类型(以 List 为例)的属性,如果 JSON 中对应的属性为空,设置为空容器而不是 null
// forContentNulls is for null array elements
mapper.configOverride(List.class)
      .setSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY));
// forValueNulls is needed for entire null array
mapper.configOverride(List.class)
      .setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));

通过 registerModule 扩展功能

支持 java.time 类型

使用 Java 8 提供的 java.time 类型(例如 java.time.LocalDateTimejava.time.OffsetDateTime)时,需要添加 com.fasterxml.jackson.datatype:jackson-datatype-jsr310 依赖并在初始化 ObjectMapper 时注册 JavaTimeModule

mapper.registerModule(new JavaTimeModule());

默认配置下 java.time.OffsetDateTime 会被序列化为一个 Unix Timestamp 如1653448397.096875000java.time.LocalDateTime 则会被序列化为一个数组如 [2022,5,25,11,13,17,96975000]。要取消这一行为(和 Spring MVC 一样处理为类似 ISO-8601 格式的字符串),使用 mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

时间对象的序列化与反序列化参考自定义序列化和反序列化部分。

支持 Optional<T>

类成员是 Optional<T> 时,序列化会获得一个 {"present":true},而反序列化会抛出 JsonMappingException 异常。

使用 com.fasterxml.jackson.datatype:jackson-datatype-jdk8 并注册 Jdk8Module,一个空的 Optional<T> 会被映射为 null,而反序列化能够正确地填充属性。

objectMapper.registerModule(new Jdk8Module());

同样支持 OptionalLongOptionalDouble

常用注解与高阶用法

@JsonProperty

修饰成员变量和方法,可在 JSON 字段名与类属性名称不一致时指定 JSON 字段的名称。index 可以指定属性在序列化结果中的顺序。

@JsonProperty 修饰的 private 属性即使没有 getter 和 setter 也能被成功序列化和反序列化。

打包处理未知字段

@JsonAnyGetter@JsonAnySetter 可以把未知或动态变化的字段通过 Map 序列化和反序列化。

public class ChangeableUser {
    private long id;
    private Map<String, String> extendsProperties = new HashMap<>();

    @JsonAnyGetter
    public Map<String, String> getExtendsProperties() {
        return extendsProperties;
    }

    @JsonAnySetter
    public void addExtendsProperty(String key, String value) {
        extendsProperties.put(key, value);
    }
}

将 JSON 字符串 {"name":"John Doe","age":24,"phone":"123456789","sex":"male","id":2301} 反序列化为 ChangeableUser 实例,可以在 extendsProperties 中访问 id 之外的属性。

忽略指定属性

@JsonIgnoreProperties 接受一个属性名称的集合,用于屏蔽 Bean 中的属性,使其不会被添加到序列化结果中,也可以使用 @JsonIgnore 注解单独设置每个属性。

@JsonIgnoreProperties({ "id", "password" })
public class SystemUser {
    private long id;
    private String name;
    private String password;
    @JsonIgnore
    private String address;
}

@JsonIgnoreType 修饰的类作为其他类的成员时,无论是序列化还是反序列化都会被忽略。

@JsonAutoDetect 注解可以根据 fieldVisibility 的值和属性是否为 public/protected/private 判断是否要在序列化/反序列化过程中处理。

自定义序列化和反序列化

一些复杂对象在序列化和反序列化过程中需要自行实现转换逻辑,例如:

  • 逗号分割的列表与 List<T> 转换
  • ISO8601 格式的日期时间字符串与 OffsetDateTime / LocalDateTime 的转换,后者需要额外处理时区信息,或改为使用 DateTimeFormatter.ISO_LOCAL_DATE_TIME

自定义序列化需要继承 StdSerializer<T> 实现 serialize 方法。

@Override
public void serialize(OffsetDateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException {
    gen.writeString(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
}

使用 @JsonSerialize(using = CustomSerializer.class) 修饰属性。

自定义的反序列化操作需要继承 StdDeserializer<T> 实现 deserialize 方法。

@Override
public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
    return LocalDateTime.parse(jsonParser.getText(), DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}

使用 @JsonDeserialize(using = CustomDeserializer.class) 修饰属性。

Chaparral

你甚至可以创建自己的 DesensitizeSerializer 以接管反序列化过程。例如笔者编写的 Chaparral - GitHub 通过这种方法实现了使用注解修饰 PII 属性后,令其在反序列化过程中将输出脱敏。

包装与拆箱

@JsonRootName

public record Cargo(String name, BigDecimal weight) {}
// 默认的序列化结果
{"name":"banana","weight":1}

@JsonRootName 注解可使用类名或 value 值作为键值包裹这个结果。

@JsonRootName("_cargo")
public record Cargo(String name, BigDecimal weight) {}
// get
{"_cargo":{"name":"banana","weight":1}}

需要 ObjectMapper 开启 (UN)WRAP_ROOT_VALUE 特性。

mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
mapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE);

@JsonUnwrapped

@JsonRootName 相反,该注解修饰的属性类可以被 flattened

public class UnwrappedUser {
    public int id;

    @JsonUnwrapped
    public Name name;

    public static class Name {
        public String firstName;
        public String lastName;
    }
}
// get
{"id":1,"firstName":"John","lastName":"Doe"}

多态类型

在一个经典的「宠物商店」场景中,可能会收到这样的请求:

[{
  "type": "Dog",
  "name": "Buddy",
  "breed": "Golden Retriever",
  "age": 3,
  "favoriteToy": "Tennis Ball"
},
{
  "type": "Cat",
  "name": "Whiskers",
  "breed": "Siamese",
  "age": 2,
  "indoor": true
},
{
  "type": "Bird",
  "name": "Tweety",
  "species": "Canary",
  "age": 1,
  "canTalk": false
}]

三种类型的宠物只有 typenameage 属性是共用的,我们可以用这三个属性设计 Pet 类,在此基础上扩展 CatDogBird 子类。

我们希望后续的 Java 逻辑可以分辨对象到底是 Cat.classDog.class 还是 Bird.class 的实例。许多同学选择将 JSON 字符串处理为 JSONObject,get("type") 后或是进行指定类型的反序列化,或是 get() 方法一条路走到黑。

这样看起来有了一些所谓的“灵活性”,不过为了这种灵活性丢失了类型信息,或让代码变得冗余实在是得不偿失。

Jackson 的 JsonTypeInfo 注解可用于处理这种多态场景:

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "Dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "Cat"),
    @JsonSubTypes.Type(value = Bird.class, name = "Bird")
})
class Pet {
    private String type;
    private String name;
    private int age;
    // ...
}

class Dog extends Pet {
    private String breed;
    private String favoriteToy;
    // ...
List<Pet> pets = objectMapper.readValue(jsonStr,
                                        new TypeReference<>() {
                                        });
assertEquals(Dog.class, pets.get(0).getClass());
assertEquals(Cat.class, pets.get(1).getClass());
assertEquals(Bird.class, pets.get(2).getClass());

使用 @JsonRawValue 输出原始值

在序列化过程中按原始值输出。

public class RawEntity {
    @JsonRawValue
    private String raw = "{\"name\":\"John Doe\"}";
}

// get
{"raw":{"name":"John Doe"}}

建造者模式

建造者模式可以用来一步步构造复杂的对象,此时对象的序列化和反序列化需要配合使用 @JsonPOJOBuilder@JsonDeserialize

@JsonDeserialize(builder = Person.Builder.class)
public class Person {
    private final String name;
    private final Integer age;

    private Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @JsonPOJOBuilder
    static class Builder {
        String name;
        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);
        }
    }
}

如果 Builder 中没有使用 withXXXbuild 方法赋值和构建对象,需要为 @JsonPOJOBuilde 注解指明 buildMethodNamewithPrefix

@JsonPOJOBuilder(buildMethodName = "create", withPrefix = "set")
static class Builder {
    String name;
    Integer age;

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

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

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