Java Jackson

Jackson注解一览

@JacksonAnnotation

这个注解经常用于Jackson自定义注解中,用来标记这是一个Jackson注解,可以用它来实现自定义的序列化注解。

@JacksonAnnotationsInside

这个注解用来标记Jackson复合注解,当使用多个Jackson注解组合成一个自定义注解时会用到它。

  1. /**
  2. * 非空以及忽略未知属性
  3. **/
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @JacksonAnnotationsInside
  6. @JsonInclude(Include.NON_NULL)
  7. @JsonIgnoreProperties(ignoreUnknown = true)
  8. public @interface NotNullAndIgnoreAnnotation {}

@JacksonInject

json属性值将在反序列化时可以被注入,先在属性上标记:

  1. @Data
  2. public final class JacksonInjectUser {
  3. @JacksonInject(value = "dynamic")
  4. private String name;
  5. private Integer age;
  6. }

然后name的值就可以在反序列化的时候动态化,不再需要去解析、拼字段。

  1. @SneakyThrows
  2. @Test
  3. void jacksonInject() {
  4. // 这个值动态化了
  5. String dynamicValue = "some Dynamic value";
  6. InjectableValues.Std injectableValues = new InjectableValues.Std()
  7. // 名称和注解中声明的相同才行
  8. .addValue("dynamic", dynamicValue);
  9. JacksonInjectUser jacksonInjectUser = objectMapper.setInjectableValues(injectableValues)
  10. // 空json 最后居然可以赋值
  11. .readValue("{}", JacksonInjectUser.class);
  12. Assertions.assertEquals(dynamicValue,jacksonInjectUser.getName());
  13. }

注意:@JacksonInject中提供了useInput参数进行绑定策略控制。

@JsonAlias

在反序列化的时候来对Java Bean的属性进行名称绑定,可以绑定多个json的键名。举个例子:

  1. @SneakyThrows
  2. @Test
  3. void jsonAlias(){
  4. // 两个json的类型结构是相同的 可以定义一个Bean来接收
  5. String userJson = "{\"name\": \"fcant.cn\",\"age\": 22}";
  6. String itemJson = "{\"category\": \"coco\", \"count\": 50 }";
  7. Domain user = objectMapper.readValue(userJson, Domain.class);
  8. Assertions.assertEquals("fcant.cn",user.getStr());
  9. Assertions.assertEquals(22,user.getNum());
  10. Domain item = objectMapper.readValue(itemJson, Domain.class);
  11. Assertions.assertEquals("coco",item.getStr());
  12. Assertions.assertEquals(50,item.getNum());
  13. }
  14. @Data
  15. public class Domain{
  16. @JsonAlias({"name","category"})
  17. private String str;
  18. @JsonAlias({"age","count"})
  19. private Integer num;
  20. }

注意:只能用于json反序列化。

@JsonAnyGetter

在json序列化时可以将Bean中的java.util.Map类型的属性“平铺展开”,举个例子:
某个Java Bean正常的json序列化结果是:

  1. {
  2. "name": "fcant.cn",
  3. "age": 22,
  4. "unMatched": {
  5. "unknown": "unknown"
  6. }
  7. }

但是需要:

  1. {
  2. "name": "fcant.cn",
  3. "age": 22,
  4. "unknown": "unknown"
  5. }

可以对Java Bean这么标记:

  1. @Data
  2. public class MapUser {
  3. private String name;
  4. private Integer age;
  5. private Map<String,Object> unMatched;
  6. @JsonAnyGetter
  7. public Map<String, Object> getUnMatched() {
  8. return unMatched;
  9. }
  10. }

然后来试一试:

  1. @SneakyThrows
  2. @Test
  3. void jsonAnyGetter(){
  4. MapUser mapUser = new MapUser();
  5. mapUser.setName("fcant.cn");
  6. mapUser.setAge(22);
  7. mapUser.setUnMatched(Collections.singletonMap("unknown","unknown"));
  8. String json = objectMapper.writeValueAsString(mapUser);
  9. // 获取json中unknown节点的值
  10. Object read = JsonPath.parse(json)
  11. .read(JsonPath.compile("$.unknown"));
  12. Assertions.assertEquals("unknown",read);
  13. }

不过这个注解的使用也是有条件的:

  • 不能是静态方法。
  • 必须是无参方法。
  • 方法的返回值必须是java.util.Map。
  • 一个实体中只能使用一个该注解。

    @JsonAnySetter

    正好和@JsonAnyGetter相反,这里就不介绍了。

    @JsonAutoDetect

    一般情况下,认为Jackson序列化对象的前提是有无参构造并且有Getter方法。事实上下面这个类依然可以序列化成json:

    1. @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
    2. public class ConstructUser {
    3. private final String name;
    4. private final Integer age;
    5. public ConstructUser(String name, Integer age) {
    6. this.name = name;
    7. this.age = age;
    8. }
    9. }

    可以通过调整Java Bean中属性、getter方法、isGetter方法、setter方法、初始化实例的方法。可见级别可以分为:

  • DEFAULT:需要根据上下文来判断,一般基于父类的可见性。

  • ANY:任何级别的都可以自动识别。
  • NONE:所有级别都不可以自动识别。
  • NON_PRIVATE:非private修饰的可以自动识别。
  • PROTECTED_AND_PUBLIC:被protectedpublic修饰的可以被自动识别。
  • PUBLIC_ONLY:只有被public修饰的才可以被自动识别。

    @JsonBackReference

    这个注解经常和另一个注解@JsonManagedReference成对出现,它为了解决递归的问题,例如两个类互相持有对方:

    1. Info info = new Info();
    2. Player player = new Player();
    3. player.setId(1);
    4. info.setPlayer(player);
    5. player.setInfo(info);
    6. // 直接无限递归了
    7. String InfiniteRecursionError = objectMapper.writeValueAsString(player);

    json序列化的时候直接无限递归了。如果想得到下面的序列化结果:
    // player

    1. {"id":1,"info":{"id":0}}

    就需要在类Player的Info属性上标记@JsonManagedReference,同时在Info类中的Player属性上标记@JsonBackReference注解。
    如果想在序列化Player时直接忽略掉Info属性,即期望得到{"id":1},只需要在Player的Info属性上标记@JsonBackReference注解。

    @JsonClassDescription

    Jackson对json schemas的支持,用来生成整个json的描述信息。

    @JsonCreator

    Jackson在反序列化时默认会去找Java Bean的无参构造,但是有些Bean没有无参构造,这时@JsonCreator就派上用场了。可以将它标记在构造方法或静态工厂方法上,通常它还需要同@JsonProperty@JacksonInject配合,就像这样:

    1. @Getter
    2. public class DescriptionUser {
    3. private final String name;
    4. private final Integer age;
    5. @JsonCreator
    6. public DescriptionUser(@JsonProperty("name") String name,
    7. @JsonProperty("age") Integer age) {
    8. this.name = name;
    9. this.age = age;
    10. }
    11. }

    对应的单元测试:

    1. @SneakyThrows
    2. @Test
    3. void jsonCreator() {
    4. String json = "{\"name\": \"fcant.cn\",\"age\": 22}";
    5. DescriptionUser user = objectMapper.readValue(json, DescriptionUser.class);
    6. Assertions.assertEquals("fcant.cn", user.getName());
    7. }

    可以在静态初始化实例工厂方法上试试这个注解。

    @JsonEnumDefaultValue

    在定义性别枚举时往往只定义了男和女两个性别。不能指望用户守规矩。科学的方法是定义一个枚举用来兜底。就像这样:

    1. public enum Gender {
    2. /**
    3. * Female gender.
    4. */
    5. FEMALE,
    6. /**
    7. * Male gender.
    8. */
    9. MALE,
    10. /**
    11. * Unknown gender.
    12. */
    13. UNKNOWN
    14. }

    当用户乱填的时候都定义为未知。在jackson反序列化支持设置一个默认值来兜底。可以在Gender#UNKNOWN上标记@JsonEnumDefaultValue,然后反序列化:

    1. @SneakyThrows
    2. @Test
    3. void jsonEnumDefaultValue(){
    4. // 开启未知枚举值使用默认值特性
    5. objectMapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
    6. String maleJson = "{\"name\": \"fcant.cn\",\"age\": 22,\"gender\":\"MALE\"}";
    7. EnumUser male = objectMapper.readValue(maleJson, EnumUser.class);
    8. Assertions.assertEquals(Gender.MALE,male.getGender());
    9. String unknownJson = "{\"name\": \"fcant.cn\",\"age\": 22,\"gender\":\"notClear\"}";
    10. EnumUser unknownGender = objectMapper.readValue(unknownJson, EnumUser.class);
    11. Assertions.assertEquals(Gender.UNKNOWN,unknownGender.getGender());
    12. }

    注意:必须手动jackson开启未知枚举值使用默认值特性。

    @JsonFilter

    同一个实体类根据不同的场景可能需要不同的序列化策略。比如对于A用户实体的某些字段可见,对于B用户另一些字段可见,实现动态的数据字段权限。这种情况下,jackson中其它一些静态注解就很难实现,借助于@JsonFilter反而简单了,下面是实现方法:

    1. // 只序列化age的策略
    2. @JsonFilter("role_a")
    3. public class OnlyAge extends FilterUser{
    4. }
    5. // 不序列化age的策略
    6. @JsonFilter("role_b")
    7. public class OnlyNameAndGender extends FilterUser{
    8. }

    接下来定义role_a和role_b的策略:

    1. @SneakyThrows
    2. @Test
    3. void jsonFilter() {
    4. SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider();
    5. // role_a只展示age
    6. SimpleBeanPropertyFilter onlyAgeFilter = SimpleBeanPropertyFilter.filterOutAllExcept("age");
    7. // role_b只排除age
    8. SimpleBeanPropertyFilter exceptAgeFilter = SimpleBeanPropertyFilter.serializeAllExcept("age");
    9. simpleFilterProvider.addFilter("role_a", onlyAgeFilter);
    10. simpleFilterProvider.addFilter("role_b", exceptAgeFilter);
    11. objectMapper.setFilterProvider(simpleFilterProvider);
    12. //被JsonFilter标记的类
    13. OnlyAge onlyAgeUser = new OnlyAge();
    14. onlyAgeUser.setName("fcant.cn");
    15. onlyAgeUser.setGender(Gender.MALE);
    16. onlyAgeUser.setAge(22);
    17. OnlyNameAndGender onlyNameAndGenderUser = new OnlyNameAndGender();
    18. onlyNameAndGenderUser.setName("fcant.cn");
    19. onlyNameAndGenderUser.setGender(Gender.MALE);
    20. onlyNameAndGenderUser.setAge(22);
    21. String onlyAge = objectMapper.writeValueAsString(onlyAgeUser);
    22. // 序列化的json中找不到name节点会抛出PathNotFoundException异常
    23. Assertions.assertThrows(PathNotFoundException.class, () -> JsonPath.parse(onlyAge)
    24. .read(JsonPath.compile("$.name")));
    25. String onlyNameAndGender = objectMapper.writeValueAsString(onlyNameAndGenderUser);
    26. // 序列化的json中找不到age节点会抛出PathNotFoundException异常
    27. Assertions.assertThrows(PathNotFoundException.class, () -> JsonPath.parse(onlyNameAndGender)
    28. .read(JsonPath.compile("$.age")));
    29. }

    思考:结合AOP甚至是Spring Security是不是有新的实现?

    @JsonFormat

    用于序列化和反序列化中特定格式的数据。虽然经常使用它来格式化时间,但是它不单单能格式化时间。

    格式化时间

    这种比较常用,主要用于格式化旧时间API: ```java @Data public class JsonFormatUser {

    @JsonFormat(shape = JsonFormat.Shape.NUMBER) private Date number; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = “yyyy-MM-dd HH:mm:ss”,timezone = “GMT+8”) private Date yyyymmdd; @JsonFormat(locale = “zh_CN”) private Date cnDate;

}

  1. 三种shape分别输出时间戳,根据时区和既定格式格式化、本地化:
  2. ```json
  3. {
  4. "number" : 1626706386340,
  5. "yyyymmdd" : "2021-07-19 22:53:06",
  6. "cnDate" : "2021-07-19T14:53:06.340+00:00"
  7. }

说实话,现在都使用新的时间API,这个注解并不推荐使用。 :::tips 注意:格式化时间需要带时区。 :::

格式化枚举

  1. public enum GenderEnum {
  2. /**
  3. * Female gender.
  4. */
  5. FEMALE("0","女"),
  6. /**
  7. * Male gender.
  8. */
  9. MALE("1","男"),
  10. /**
  11. * Unknown gender.
  12. */
  13. @JsonEnumDefaultValue
  14. UNKNOWN("-1","未知");
  15. private final String value;
  16. private final String description;
  17. GenderEnum(String value, String description) {
  18. this.value = value;
  19. this.description = description;
  20. }
  21. public String getValue() {
  22. return value;
  23. }
  24. public String getDescription() {
  25. return description;
  26. }
  27. }

上面这种枚举类只能格式化成枚举名称,很多时候期望能够获取键值对的枚举格式,例如GenderEnum.FEMALE

  1. {"value":"0","description":"女"}

只需要使用@JsonFormatshape特性:

  1. @JsonFormat(shape = JsonFormat.Shape.OBJECT)
  2. public enum GenderEnum {
  3. // 省略
  4. }

@JsonGetter@JsonGetter

json序列化和反序列化时指定属性的Getter和Setter方法。特别针对有些不正规的方法,同时还可以指定别名,例子:

  1. public class GetterAndSetter {
  2. private String name;
  3. @JsonGetter("n")
  4. public String name(){
  5. return this.name;
  6. }
  7. @JsonSetter("name")
  8. public void name(String name){
  9. this.name= name;
  10. }
  11. }

断言测试:

  1. GetterAndSetter getterAndSetter = new GetterAndSetter();
  2. getterAndSetter.name("fcant.cn");
  3. String s = objectMapper.writeValueAsString(getterAndSetter);
  4. Object n = JsonPath.parse(s)
  5. .read(JsonPath.compile("$.n"));
  6. Assertions.assertEquals("fcant.cn",n);
  7. String json = "{\"name\":\"fcant.cn\"}";
  8. GetterAndSetter getAndSet = objectMapper.readValue(json, GetterAndSetter.class);
  9. Assertions.assertEquals("fcant.cn",getAndSet.name());

大部分情况下这两个注解比JsonProperty注解更加通用。

@JsonIdentityInfo

这个作用于类或属性上,被用来在序列化/反序列化时为该对象或字段添加一个对象识别码,比如@id或者Class对象名,主要解决字段循环嵌套的问题,例如数据库中的多对多关系,Bean嵌套依赖。开发ORM的相关功能时会用到。 :::info 扩展:@JsonIdentityReference 具有类似的功能,强调了使用id作为标识。 :::

@JsonIgnore

这个也是常用的一个注解。在序列化/反序列化时忽略被该注解标记的属性。这个注解和前面介绍的@JsonFilter提供的功能差不多。不过该注解是静态标记。 :::tips 注意:JsonProperty注解的access也可以实现该注解的功能,不建议两个注解混用,这样可能发生冲突。 :::

@JsonIgnoreProperties

这个也经常使用。在序列化/反序列化时忽略多个属性,标记在类上。例如忽略internalIdsecretKey属性:

  1. @JsonIgnoreProperties({ "internalId", "secretKey" }

如果有些属性不太确定也可以通过该注解过滤掉,避免未知属性异常:

  1. @JsonIgnoreProperties(ignoreUnknown=true)

@JsonIgnoreType

在序列化/反序列化时如果希望忽略掉某种特定类型可以借助于该注解:

  1. @JsonIgnoreType
  2. class Credentials {
  3. public String password;
  4. }
  5. class Settings {
  6. public int userId;
  7. public String name;
  8. public Credentials pwd;
  9. }

Settings进行序列化和反序列化时Credentials将会被忽略掉。主要用来对一些数据敏感的对象进行忽略,比如用户的凭据。

@JsonInclude

用于指示属性何时可以被序列化,可以把该注解标记到属性字段上,也可以通过setSerializationInclusion 方法统一设置。常用的JsonInclude.Include.NON_NULL可以过滤空值:

  1. Player player = new Player();
  2. player.setId(1);
  3. player.setName(null);

对应:

  1. {
  2. "id":1
  3. }

其它策略参见JsonInclude.Include。 :::info 扩展:使用CUSTOM策略时可以实现自定义测过滤方法。 :::

@JsonIncludeProperties

这个注解机制有点类似@JsonIgnoreProperties,只不过它的功能和@JsonIgnoreProperties相反。如果一个类标记了这个注解:

  1. @JsonIncludeProperties({ "internalId", "secretKey" })

除了internalIdsecretKey属性,其它属性都不参与序列化和反序列化。

@JsonProperty

@JsonProperty也是常用注解。用来标记属性或者属性的gettersetter方法上,用于指定属性的json名称,类似@JsonAlias的效果,同时配合其Access枚举可以实现那些属性可以序列化,那些属性可以反序列化(类似忽略的效果)。

  1. @Data
  2. public class MapUser {
  3. @JsonProperty(value = "myname")
  4. private String name;
  5. @JsonProperty(value = "a")
  6. private Integer age;
  7. }
  8. // {"myname":"fcant.cn","a":22,"}

小结

Jackson是一款非常优秀的json类库,提供了丰富的注解来满足各种场景的需要。根据日常一些场景的需要结合这些注解设计了不少动态的、可扩展的、通用的序列化和反序列化功能,用起来非常方便顺手。