jackson 是 java 编程领域应用广泛的 json 处理工具,同时也是 spring 默认的序列化实现。本文总结了开发中常用的一些 jackson 技巧,注意本文只涉及本人开发中最常用的技巧,一些冷门用法并未提供。如果日后工作中遇到一些使用场景,本文会陆续更新。

下面示例代码中的JsonUtils​是包装了 ObjectMapper 的工具类,直接使用 ObjectMapper 也可以。

本文所有技巧都基于 jackson 系列 2.14.3版本

实用注解

@JsonValue

​@JsonValue​注解最常用在类字段上,特别是在枚举中的使用很广泛。当字段加了@JsonValue​注解后,表明该类在序列化时会使用该字段作为序列化后的值。
例如下面代码:

@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class Person {  
  
    @JsonValue  
    private String name;  
  
    private Integer age;  
}
 public class JacksonTest {  
  
    public static void main(String[] args) {  
        String json = JsonUtils.toJson(new Person("mahaonan", 30));  
        System.out.println(json); //mahaonan  
    }  
}

序列化 Person 时,会直接序列化为@JsonValue​指定的属性 name​的值
特别的,对于枚举类型,@JsonValue​ 除了可以指定序列化的值,还可以用于反序列化。
例如下面代码:

@Getter  
public enum OperatorEnum {  
  
  
    EQ("eq", "=", "等于"),  
    ;  
  
    @JsonValue  
    private final String code;  
    private final String sqlOperator;  
    private final String description;  
  
    OperatorEnum(String code, String sqlOperator, String description) {  
        this.code = code;  
        this.sqlOperator = sqlOperator;  
        this.description = description;  
    }  
}
public class JacksonTest {  
  
    public static void main(String[] args) {  
        String json1 = JsonUtils.toJson(OperatorEnum.EQ);  
        System.out.println(json1); //"eq"  
        OperatorEnum parse1 = JsonUtils.parse("\"eq\"", OperatorEnum.class);  
        System.out.println(parse1); //EQ  
    }  
}

可见,枚举类型的序列化和反序列化都依托于该注解。

注意
枚举反序列化在该版本(2.14.3)的jackson 中是支持的,旧版本可能不支持,如果发现不支持,请升级版本。

除了用于属性上,@JsonValue​还可用于方法上,用法类似,当作用于方法时,代表将方法的返回值作为序列化的结果。

@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class Person {  
  
    private String name;  
  
    private Integer age;  
  
    @JsonValue  
    public String serialize() {  
        return "name:" + name + ", age:" + age;  
    }  
}
public class JacksonTest {  
  
    public static void main(String[] args) {  
        String json1 = JsonUtils.toJson(new Person("zhangsan", 18));  
        System.out.println(json1); // "name:zhangsan, age:18"  
    }  
}

需要注意的是,无论@JsonValue​作用于哪里,在一个类中最多只能有一个该注解。

@JsonProperty

@JsonProperty​ 通常用于类字段上,用于标明该字段在序列化/反序列化时的名称。
使用场景多见于 json 和 java 实体类字段定义不一致的时候。
例如如下代码:

@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class Person {  
  
    @JsonProperty("_name")  
    private String name;  
    private Integer age;  
  
}
public class JacksonTest {  
  
    public static void main(String[] args) {  
        String json1 = JsonUtils.toJson(new Person("zhangsan", 18));  
        System.out.println(json1); // {"age":18,"_name":"zhangsan"}  
    }  
}

@JsonIgnore

@JsonIgnore​ 通常用于类字段上,用于标明该字段在序列化/反序列化时忽略。
例如如下代码:

@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class Person {  
  
    @JsonIgnore  
    private String name;  
    private Integer age;  
  
}
public class JacksonTest {  
  
    public static void main(String[] args) {  
        String json1 = JsonUtils.toJson(new Person("zhangsan", 18));  
        System.out.println(json1);  //{"age":18}
    }  
}

@JsonInclude

@JsonInclude​ 用来控制 不同字段值情况下输出 json 的格式。
也就是说,假如你需要某个字段为 null 的时候不序列化,那么可以使用该注解。
该注解可以用在属性和类上,如果用于类,表明该类下所有字段都采用该策略。
JsonInclude.Include​ 枚举值有 7 种,这里只介绍最常用的几种,其余的可以参考源码注释。

  • ALWAYS:默认策略,任何情况下都包含该属性。

  • NON_NULL:只有当属性不为 null 时才会被包含。

  • CUSTOM:使用自定义的 ValueFilter 来确定是否序列化。

[!warning] 注意
其他的例如NON_EMPTY,NON_DEFAULT使用时一定要注意影响范围,建议多尝试下,可能会排除一些你认为无需排除的值。

以上三种是开发中最常用的策略,一般情况下有NON_NULL​就足够了。如果有一些特别的定制化逻辑,推荐使用自定义的ValueFilter​来进行判断。
​NON_NULL​使用见如下代码:

@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class Person {  
  
    @JsonInclude(JsonInclude.Include.NON_NULL)  
    private String name;  
    private Integer age;  
  
}
public class JacksonTest {  
  
    public static void main(String[] args) {  
        String json1 = JsonUtils.toJson(new Person(null, 18));  
        System.out.println(json1); // {"age":18}  
    }  
}

假如我们有这样的需求,名称为 null 或者包含zhangsan​的不序列化,那么可以使用ValueFilter​。见如下代码

@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class Person {  
  
    @JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = FilterNameInclude.class)  
    private String name;  
    private Integer age;  
  
}
public class FilterNameInclude {  
  
    @Override  
    public boolean equals(Object obj) {  
        if (obj == null) {  
            return true;  
        }  
        if (obj instanceof String) {  
            String str = (String) obj;  
            return str.contains("zhangsan");  
        }  
        return false;  
    }  
}
public class JacksonTest {  
  
    public static void main(String[] args) {  
        String json1 = JsonUtils.toJson(new Person("zhangsan", 18));  
        System.out.println(json1); // {"age":18}  
        String json2 = JsonUtils.toJson(new Person("zhangsan", 18));  
        System.out.println(json2); // {"age":18}  
        String json3 = JsonUtils.toJson(new Person("lisi", 18));  
        System.out.println(json3); // {"name":"lisi","age":18}  
    }  
}

可见,只需要在某个类中定义 equals​逻辑,如果返回 true,说明不序列化。

@JsonFormatter

用于控制时间序列化/反序列化时的格式,常见的用法如下:

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date birthday;

常用场景

全局设置常用参数

在开发中,我们通常会全局定义一个 ObjectMapper​对象,然后配置一些默认的规则,这样就不用每次都新建一个。除此之外,配置好一些规则后,一些注解也可以不再使用(例如@JsonFormatter​)。
下面是我日常开发中常用的一些配置项:

通用配置

public static void fillDefaultProperty(ObjectMapper mapper) {  
    //支持单引号  
    mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);  
    //反序列化时忽略未知属性字段  
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);  
    //空字符串反序列化到Object时,设置为NULL  
    mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);  
    //空数组反序列化到Object时,设置为NULL  
    mapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true);  
    //支持单值的对象反序列到数组  
    mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);  
    //忽略无法转换的对象  
    mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);  
}

如果需要默认开启为 null 不序列化,可以设置如下属性

mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 

日期配置

public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";  
public static final String DATE_FORMAT = "yyyy-MM-dd";
/**  
 * 适配localDateTime  
 * * @param mapper  
 */  
public static void adapterLocalDate(ObjectMapper mapper) {  
    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);  
    mapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);  
    SimpleModule javaTimeModule = new SimpleModule();  
    javaTimeModule.addSerializer(Date.class, new DateSerializer(false, new SimpleDateFormat(DATE_TIME_FORMAT)));  
    javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));  
    javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));  
    javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer());  
    javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));  
    javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));  
    mapper.registerModule(javaTimeModule);  
    mapper.setTimeZone(TimeZone.getDefault());  
}

在使用了该日期配置后,除了支持LocalTime​和LocalDateTime​之外,也无需在字段上添加@JsonFormatter​即可实现默认的yyyy-MM-dd HH:mm:ss​格式。

[!NOTE] Tips
如果是 Springboot 项目,可以在修改内置的 ObjectMapper 使其适配日期,这样前后端交互的 DTO 中就不需要设置日期格式了!

@Override  
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {  
    converters.forEach(converter -> {  
        if (converter instanceof MappingJackson2HttpMessageConverter) {  
            MappingJackson2HttpMessageConverter jacksonConverter =  
                    (MappingJackson2HttpMessageConverter) converter;  
            ObjectMapper mapper = jacksonConverter.getObjectMapper();  
            JacksonUtils.fillDefaultProperty(mapper);  
            JacksonUtils.adapterLocalDate(mapper);  
            mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);  
            ((MappingJackson2HttpMessageConverter) converter).setObjectMapper(mapper);  
        }  
    });  
}

序列化时保留类型信息

在实际开发中,我们有时候会有这样的情况,属性是一个 Object​对象。此时序列化时是没有问题的。但是反序列化的时候,由于 jackson 并不知道这个 json 对应的实际类型是啥,因此就会按默认的策略执行:

  • 数组反序列化为 ArrayList

  • 对象反序列化为 LinkedHashMap

  • 其他基本类型按预设反序列化
    这样我们就会丢掉之前的对象信息,可能会导致一些问题。此时我们就可以设置序列化时保留类型信息,这样反序列化的时候就可以反序列化到我们之前的对象。

/**  
 * 序列化时保留实际类型信息  
 *  
 * @param mapper  
 */  
public static void enableDefaultTyping(ObjectMapper mapper) {  
    BasicPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()  
            .allowIfBaseType(Object.class)  
            .build();  
    mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);  
}

父子类多态序列化

在实际开发中,有时候会有这样的设计,有一个父类,然后会有一堆子类拥有各自的属性,不同的字段。父类中有一个类型的枚举,用于区分不同的子类型。例如在报表设计中,父类是通用的图表定义,子类可能有折线图,柱状图,饼图等等。如下代码示例:

@Data  
public class ChartAxisConfigDTO {  
  
    @ApiModelProperty("图表类型")  
    private ChartTypeEnum chartType;  
}
/**  
 * 折线图轴配置  
 * @author mahaonan  
 */@EqualsAndHashCode(callSuper = true)  
@EqualsAndHashCode(callSuper = true)  
@Data  
public class LineAxisConfigDTO extends ChartAxisConfigDTO{  
  
    /**  
     * 类别轴/维度  
     */  
    @JsonProperty("xAxis")  
    private List<ChartFieldDTO> xAxis;  
  
    /**  
     * 子类别/维度  
     */  
    @JsonProperty("xSubAxis")  
    private List<ChartFieldDTO> xSubAxis;  
  
    /**  
     * 值轴/指标  
     */  
    @JsonProperty("yAxis")  
    private List<ChartFieldDTO> yAxis;  
}
/**  
 * 饼图轴配置  
 * @author mahaonan  
 */
@EqualsAndHashCode(callSuper = true)  
@Data  
public class PieAxisConfigDTO extends ChartAxisConfigDTO {  
  
    /**  
     * 扇区标签/维度  
     */  
    @JsonProperty("xAxis")  
    private List<ChartFieldDTO> xAxis;  
  
    /**  
     * 扇区标签/指标  
     */  
    @JsonProperty("yAxis")  
    private List<ChartFieldDTO> yAxis;  
}

此时,序列化时是没问题的,jackson 会自动根据对象的实际类型来进行序列化。但是反序列化时,jackson 并不知道要序列化成哪个子对象。例如对于如下的 json 字符串,反序列化时只能反序列化为父类,导致丢失属性。

{
    "chartType": 3,
    "xAxis": [
        {}
    ],
    "yAxis": [
        {}
    ]
}

chartType=3 对应饼图,我们期望是序列化成饼图对应的类。

这种情况下,可以使用如下技巧。
jackson 提供了两个注解@JsonTypeInfo​和@JsonSubTypes​
​@JsonTypeInfo​用于处理多态类型,可以指定类型信息的包含方式,以及如何使用该信息来选择合适的类进行反序列化。
​@JsonSubTypes​则是配合@JsonTypeInfo​,用于指明什么情况下用什么类进行操作。
对于上述的场景,我们只需要按如下代码设置:

@Data  
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "chartType", visible = true, include = JsonTypeInfo.As.EXISTING_PROPERTY)  
@JsonSubTypes({  
    @JsonSubTypes.Type(value = ChartAxisConfigDTO.class, name = "1"),  
    @JsonSubTypes.Type(value = LineAxisConfigDTO.class, name = "2"),  
    @JsonSubTypes.Type(value = PieAxisConfigDTO.class, name = "3"),  
})  
public class ChartAxisConfigDTO {  
  
    @ApiModelProperty("图表类型")  
    private ChartTypeEnum chartType;  
}

​@JsonTypeInfo​各种设置,就是告诉 jackson 我们使用了名为chartType​的类型信息,并且要求在反序列化为 java 对象后保留该字段(visible = true)。同时,这个字段是我们预先就设置的,不需要额外添加该类型信息。
​@JsonSubTypes​ 则规定了 chartType 的值是多少时,用哪个子类进行反序列化。

泛型处理

泛型的反序列化是实际开发中很头疼也很常见的,jackson 在这方面的使用也很方便。
此时需要用到 Jackson 提供的 TypeFactory

public JavaType constructParametricType(Class<?> parametrized, Class<?>... parameterClasses) {  
    int len = parameterClasses.length;  
    JavaType[] pt = new JavaType[len];  
    for (int i = 0; i < len; ++i) {  
        pt[i] = _fromClass(null, parameterClasses[i], EMPTY_BINDINGS);  
    }  
    return constructParametricType(parametrized, pt);  
}

parametrized​:类型擦除类型,也就是外面的类型,例如 List.class
parameterClasses​:泛型对象参数,也就是<>​里面的类型,例如 Person.class,有多个泛型类型就传入多个

集合类型

public class JacksonTest {  
  
    public static final ObjectMapper mapper = new ObjectMapper();  
  
    public static JavaType constructGenericType(Class<?> genericClass, Class<?>... parameterClasses) {  
        return mapper.getTypeFactory().constructParametricType(genericClass, parameterClasses);  
    }  
  
    public static void main(String[] args) throws Exception{  
        // 将数组饭反序列化为List<Person>  
        List<Person> personList = mapper.readValue("[{\"name\":\"zhangsan\",\"age\":18,\"birthday\":\"2021-08-01 00:00:00\"}]", constructGenericType(List.class, Person.class));  
    }  
}

多层嵌套

对于嵌套的 list,需要用如下的方法构造

/**  
 * 构造嵌套List类型  
 */  
public static JavaType constructNestListType(Class<?> elementClass) {  
    TypeFactory typeFactory = mapper.getTypeFactory();  
    JavaType innerListType = typeFactory.constructCollectionType(List.class, elementClass);  
    return typeFactory.constructCollectionType(List.class, innerListType);  
}
public class JacksonTest {  
  
    public static final ObjectMapper mapper = new ObjectMapper();  

    public static void main(String[] args) throws Exception{  
        List<List<Person>> personList2 = mapper.readValue("[[{\"name\":\"zhangsan\",\"age\":18,\"birthday\":\"2021-08-01 00:00:00\"}]]", constructNestListType(Person.class));  
System.out.println(personList2);
    }  
}

Res统一响应

开发中常常会定义统一的返回值包装,例如

@Data  
public class Res<T> {  
  
    private Integer code;  
    private String message;  
    private T data;  
}

对于这种对象的反序列化也很简单,和上面一致。如果 T 是一个简单类型,一层包装即可。如果是 List 这种集合类型,嵌套一层即可。总之就一个原则,JavaType​需要构建出匹配的泛型对象和参数对象。

public static void main(String[] args) throws Exception{  
    JavaType javaType = constructGenericType(Res.class, constructGenericType(List.class, Person.class).getRawClass());  
    Res<List<Person>> res = mapper.readValue("{\"code\":200,\"message\":\"success\",\"data\":[{\"name\":\"zhangsan\",\"age\":18,\"birthday\":\"2021-08-01 00:00:00\"}]}", javaType);  
    System.out.println(res);  
}

或者,还有下面一种办法,利用TypeReference​实现。

public static void main(String[] args) throws Exception{  
    Res<List<Person>> res2 = mapper.readValue("{\"code\":200,\"message\":\"success\",\"data\":[{\"name\":\"lisi\",\"age\":20,\"birthday\":\"2021-08-01 00:00:00\"}]}", new TypeReference<Res<List<Person>>>() {  
    });  
    System.out.println(res2);  
}

这种方法更加直接,什么样的嵌套对象,就创建什么样的TypeReference泛型即可。
总之,对于泛型的处理,用好 TypeFactory即可。