Jackson学习笔记(二)

Posted by lili on March 21, 2022

本文介绍怎么在Jackson里实现多态类的序列化与反序列化。

目录

简介

在Java中,我们通常会定义基类(接口/抽象类),然后实际用到是其子类。这会在反序列化的时候带来问题,因为JSON本身是没有schema的,它在反序列化的时候根本不知道应该用哪个子类来对应。所以为了能够反序列化,通常要在JSON里加入一些额外的信息才能正确的反序列化。注意:这样的Java对象序列化成JSON后其它语言可能很难做对应的反序列化(到对象),因此更多的用于Java语言内部使用。请在设计对外的API接口时尽量不用这样的POJO!

测试POJO类

我们首先定义几个类:

@Data
@NoArgsConstructor
@AllArgsConstructor
public abstract class Vehicle {
    private String make;
    private String model;
}

Vehicle是一个抽象的类,包括制造商(make)和车型(model)两个字段。

@Data
@NoArgsConstructor
public class Car extends Vehicle {
    private int seatingCapacity;
    private double topSpeed;

    public Car(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
        this.topSpeed = topSpeed;
    }

}

Car是Vehicle的一个子类,它增加了座位数(seatingCapacity)和最高时速(topSpeed)两个字段。

@Data
@NoArgsConstructor
public final class Truck extends Vehicle {
    private double payloadCapacity;

    public Truck(String make, String model, double payloadCapacity) {
        super(make, model);
        this.payloadCapacity = payloadCapacity;
    }

}

Truck是另一个子类,它增加了载重(payload capacity)这个字段。

@Data
@NoArgsConstructor
public class Trailer extends Car{
    private String trailerType;

    public Trailer(String trailerType, String make, String model,
                   int seatingCapacity, double topSpeed){
        super(make, model, seatingCapacity, topSpeed);
        this.trailerType = trailerType;
    }
}

Trailer(房车)是Car的一个子类,它增加了一个拖车类型的字段。

子类的问题

我们首先定义一个Cars类,假设用它来存储多个Car。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Cars {
    private List<Car> cars;
}

下面是测试代码:

public class TestCarList {
    public static void main(String[] args) throws JsonProcessingException {
        List<Car> carList = new ArrayList<>();
        carList.add(new Car("BMW", "S500", 5, 250.0));
        carList.add(new Car("BMW", "S600", 5, 300.0));
        Cars cars = new Cars(carList);

        ObjectMapper objectMapper = new ObjectMapper();
        ObjectWriter writer = objectMapper.writer(new DefaultPrettyPrinter());
        String json = writer.writeValueAsString(cars);
        System.out.println(json);


        Cars cars2 = objectMapper.readValue(json, Cars.class);
        json = writer.writeValueAsString(cars2);
        System.out.println(json);
    }
}

上面的代码是没有任何问题的,因为Cars类定义里保存了List的信息,所以反序列化的时候Jackson知道List里的对象是Car类的。但是如果我们往cars里放一个Car的子类呢?比如下面的代码:

public class TestCarList2 {
    public static void main(String[] args) throws JsonProcessingException {
        List<Car> carList = new ArrayList<>();
        carList.add(new Car("BMW", "S500", 5, 250.0));
        carList.add(new Car("BMW", "S600", 5, 300.0));
        carList.add(new Trailer("a", "BMW", "S700", 8, 150.));
        Cars cars = new Cars(carList);

        ObjectMapper objectMapper = new ObjectMapper();
        ObjectWriter writer = objectMapper.writer(new DefaultPrettyPrinter());
        String json = writer.writeValueAsString(cars);
        System.out.println(json);


        Cars cars2 = objectMapper.readValue(json, Cars.class);
        json = writer.writeValueAsString(cars2);
        System.out.println(json);
    }
}

上面的代码里,我们往carList里放了一个Trailer对象。把Cars变成JSON是没有问题的,其输出为:

{
  "cars" : [ {
    "make" : "BMW",
    "model" : "S500",
    "seatingCapacity" : 5,
    "topSpeed" : 250.0
  }, {
    "make" : "BMW",
    "model" : "S600",
    "seatingCapacity" : 5,
    "topSpeed" : 300.0
  }, {
    "make" : "BMW",
    "model" : "S700",
    "seatingCapacity" : 8,
    "topSpeed" : 150.0,
    "trailerType" : "a"
  } ]
}

但是反序列化时会抛出异常:com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field “trailerType”。因为Jackson看到的是List,所以它会尝试把Trailer输出的json转成Car。但是Trailer有一个额外的自动"trailerType",因此会抛出异常。读者可能会说,Jackson怎么这么笨,明显我只定义了Car的一个子类,它正好有trailerType字段,它怎么不能猜出这是一个Trailer对象而不是Car?当然它不能乱猜,猜错了就出问题了。我们难道能认为两个类的自动相同就是同一个类吗?显然不行,因为万一后面我们需要根据不同的Car类型算价钱,不同的类型的价格可能不同,那问题可就严重了。读者可能又会说,那我们能不能让Jackson忽略位置的属性呢?通过objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 这样不就没有问题了吗?我们当然可以这样做,但是通常我们不会希望Jackson把最后一个Trailor反序列化成Car,因为这明显丢失了重要的trailerType信息。

类似的,我们通常会定义抽象基类或者接口,如下:

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Fleet {
    private List<Vehicle> vehicles;
}

那么也会出现错误,测试代码如下:

public class TestPolymorphicType {
    public static void main(String[] args) throws JsonProcessingException {
        List<Vehicle> vehicleList = new ArrayList<>();
        vehicleList.add(new Car("BMW", "S500", 5, 250.0));
        vehicleList.add(new Truck("Isuzu", "NQR", 7500.0));

        Fleet fleet = new Fleet(vehicleList);

        ObjectMapper objectMapper = new ObjectMapper();
        ObjectWriter writer = objectMapper.writer(new DefaultPrettyPrinter());
        String json = writer.writeValueAsString(fleet);
        System.out.println(json);


        Fleet fleet2 = objectMapper.readValue(json, Fleet.class);
        json = writer.writeValueAsString(fleet2);
        System.out.println(json);
    }
}

异常为:InvalidDefinitionException: Cannot construct instance of com.github.fancyerii.learnjackson.a02.Vehicle。很显然,它尝试构造Vehicle对象,但是Vehicle是个抽象类,不可能构造出来。

设置全局默认多态类型

为了解决这个问题,Jackson可以设置ObjectMapper的全局默认多态类型。具体来说通过如下函数设置:

ObjectMapper.activateDefaultTyping(PolymorphicTypeValidator ptv, 
  ObjectMapper.DefaultTyping applicability, JsonTypeInfo.As includeAs)

这个方法看起来有点复杂,我们通过一个具体的例子来看看它的用法:

public class TestDefaultTyping {
    public static void main(String[] args) throws JsonProcessingException {
        List<Vehicle> vehicleList = new ArrayList<>();
        vehicleList.add(new Car("BMW", "S500", 5, 250.0));
        vehicleList.add(new Truck("Isuzu", "NQR", 7500.0));

        Truck.Fleet fleet = new Truck.Fleet(vehicleList);


        PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
                .allowIfSubType("com.github.fancyerii.learnjackson.a02.pojo")
                .allowIfSubType("java.util.ArrayList")
                .build();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);

        ObjectWriter writer = objectMapper.writer(new DefaultPrettyPrinter());
        String json = writer.writeValueAsString(fleet);
        System.out.println(json);


        Truck.Fleet fleet2 = objectMapper.readValue(json, Truck.Fleet.class);
        json = writer.writeValueAsString(fleet2);
        System.out.println(json);
    }
}

这个例子和前面的基本一样,注意的区别是调用了ObjectMapper.activateDefaultTyping方法。我们首先来看程序的输出:

{
  "@class" : "com.github.fancyerii.learnjackson.a02.Fleet",
  "vehicles" : [ "java.util.ArrayList", [ {
    "@class" : "com.github.fancyerii.learnjackson.a02.pojo.Car",
    "make" : "BMW",
    "model" : "S500",
    "seatingCapacity" : 5,
    "topSpeed" : 250.0
  }, {
    "@class" : "com.github.fancyerii.learnjackson.a02.pojo.Truck",
    "make" : "Isuzu",
    "model" : "NQR",
    "payloadCapacity" : 7500.0
  } ] ]
}

这个Json和前面有点区别,我们再和之前没有调用activateDefaultTyping方法的对比一下:

{
  "vehicles" : [ {
    "make" : "BMW",
    "model" : "S500",
    "seatingCapacity" : 5,
    "topSpeed" : 250.0
  }, {
    "make" : "Isuzu",
    "model" : "NQR",
    "payloadCapacity" : 7500.0
  } ]
}

细心的读者可能发现其中的区别:新的Json把原来的每个输出都变成了一个Array,并且第一个元素是类型信息,第二个才是真正的json。比如我们看下面的部分:

[ "com.github.fancyerii.learnjackson.a02.pojo.Car", {
    "make" : "BMW",
    "model" : "S500",
    "seatingCapacity" : 5,
    "topSpeed" : 250.0
  } ]

原来的那个{}对象变成了一个数组[],数组的第一个元素是个字符串,值为对象的全限定名称,第二个元素才是真正对象转换的json。

有的读者可能觉得这样保存类型信息看起来很乱,因为这个json的结构和原来完全不同,如果是别的语言的客户端看到之后会很奇怪。我们可以修改activateDefaultTyping方法的第三个参数,把它从JsonTypeInfo.As.WRAPPER_ARRAY(默认值)改成JsonTypeInfo.As.PROPERTY,那么结果如下:

{
  "@class" : "com.github.fancyerii.learnjackson.a02.pojo.Truck$Fleet",
  "vehicles" : [ "java.util.ArrayList", [ {
    "@class" : "com.github.fancyerii.learnjackson.a02.pojo.Car",
    "make" : "BMW",
    "model" : "S500",
    "seatingCapacity" : 5,
    "topSpeed" : 250.0
  }, {
    "@class" : "com.github.fancyerii.learnjackson.a02.pojo.Truck",
    "make" : "Isuzu",
    "model" : "NQR",
    "payloadCapacity" : 7500.0
  } ] ]
}

这样看起来是不是好多了?我们看到,它把某个对象的类型信息用一个特殊的@class属性来保存,这样整体JSON的结构就没有变化。但是注意:JsonTypeInfo.As.PROPERTY只会在对象里增加@class属性,如果是一个Array,它还是会用数组的第一个字段来保存类型信息。

接下来我们来看第一个参数,它的类型是PolymorphicTypeValidator,它是通过BasicPolymorphicTypeValidator的builder方法构造的:

PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
        .allowIfSubType("com.github.fancyerii.learnjackson.a02.pojo")
        .allowIfSubType("java.util.ArrayList")
        .build();

它的意思是对于com.github.fancyerii.learnjackson.a02.pojo包下的所有类或者java.util.ArrayList(及其子类)都保存类的信息。因为我们通常只是对于那些有多态的类才需要增加类型信息,而且如果所有的类都尝试,可能也会有安全漏洞。所以我们通过PolymorphicTypeValidator来告诉ObjectMapper哪些类序列化的时候需要额外增加类型信息。

注意:如果ObjectMapper设置了PolymorphicTypeValidator,即使输入的JSON没有@class,也是不能序列化的,比如:

        PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
                .allowIfSubType("com.github.fancyerii.learnjackson.a02.pojo")
                .build();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        MyBean myBean = new MyBean(1, "hi");
        String json = objectMapper.writeValueAsString(myBean);
        System.out.println(json);
        MyBean myBean1 = objectMapper.readValue("\"intValue\":1,\"strValue\":\"hi\"}", MyBean.class);

我们配置了只能反序列化com.github.fancyerii.learnjackson.a02.pojo包,而我们的MyBean不是这个包下的,那么就不能反序列化。

最后是第二个参数,它告诉ObjectMapper哪些类序列化的时候需要输出类型信息。我们这里设置的是NON_FINAL,也就是非Final的类型。因为Final类型没有多态的问题,是没有必要保留类型信息的。注意:这里的final指的是我们定义的时候,在Fleet类里我们定义了private List vehicles,即使我们往里面放了final的Truck,它也会保存类型信息。但是如果我们定义List,就不会保存,比如:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TruckList {
    private List<Truck> trucks;
}

public class TestDefaultTyping4 {
    public static void main(String[] args) throws JsonProcessingException {
        List<Truck> trucks = new ArrayList<>();
        trucks.add(new Truck("Isuzu", "NQR", 7500.0));

        TruckList truckList = new TruckList(trucks);


        PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
                .allowIfSubType("com.github.fancyerii.learnjackson.a02")
                .allowIfSubType("java.util.ArrayList")
                .build();
        ObjectMapper objectMapper = new ObjectMapper();

        objectMapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        ObjectWriter writer = objectMapper.writer(new DefaultPrettyPrinter());
        String json = writer.writeValueAsString(truckList);
        System.out.println(json);


        TruckList truckList2 = objectMapper.readValue(json, TruckList.class);
        json = writer.writeValueAsString(truckList2);
        System.out.println(json);
    }
}

我们看到输出为:

{
  "@class" : "com.github.fancyerii.learnjackson.a02.TruckList",
  "trucks" : [ "java.util.ArrayList", [ {
    "make" : "Isuzu",
    "model" : "NQR",
    "payloadCapacity" : 7500.0
  } ] ]
}

List是保存了其类型信息(java.util.ArrayList,但是里面的内容没有保存Truck的类型信息。

类级别的注解

前面的全局设置比较简单,但是我们发现它会把所有的非final类都假设类型信息。其实一般我们大部分类虽然不是final,但是通常也没有子类,增加那么多信息让JSON很冗余。为了解决这个问题,Jackson提供了类级别的注解,让我们只对某些类增加类型信息以实现多态的序列化和反序列化。

为了实现Vehicle及其子类的多态,我们需要修改Vehicle,增加一些注解:

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "@type")
@JsonSubTypes({
        @JsonSubTypes.Type(value = Car.class, name = "car"),
        @JsonSubTypes.Type(value = Truck.class, name = "truck")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
public abstract class Vehicle {
    private String make;
    private String model;
}

这里需要两个注解,第一个是JsonTypeInfo,它说明Vehicle及其子类在序列化的时候需要保存额外的类型信息。那怎么区分不同的子类呢?就是通过一个特别的名字(JsonTypeInfo.Id.NAME),并且把这个名字作为类的一个特殊属性存储,这个特殊属性的名字叫做”@type”。属性名字是任意的,为了不与正常的属性冲突,我们给它起名@type。

那么细心的读者可能会问题,不同的子类名字在哪里呢?这就是下面的注解的作用。JsonSubTypes为子类定义一个特殊的名字,这里把Car.class起名”car”,Truck.class起名”truck”。这里的起名也是任意的(当然不能重名,另外也需要好记)。

好了,接下来我们测试一下:

public class TestPolymorphicType2 {
    public static void main(String[] args) throws JsonProcessingException {
        List<Vehicle> vehicleList = new ArrayList<>();
        vehicleList.add(new Car("BMW", "S500", 5, 250.0));
        vehicleList.add(new Truck("Isuzu", "NQR", 7500.0));

        Fleet fleet = new Fleet(vehicleList);

        ObjectMapper objectMapper = new ObjectMapper();
        ObjectWriter writer = objectMapper.writer(new DefaultPrettyPrinter());
        String json = writer.writeValueAsString(fleet);
        System.out.println(json);


        Fleet fleet2 = objectMapper.readValue(json, Fleet.class);
        json = writer.writeValueAsString(fleet2);
        System.out.println(json);
    }
}

我们看一下序列化后的结果:

{
  "vehicles" : [ {
    "@type" : "car",
    "make" : "BMW",
    "model" : "S500",
    "seatingCapacity" : 5,
    "topSpeed" : 250.0
  }, {
    "@type" : "truck",
    "make" : "Isuzu",
    "model" : "NQR",
    "payloadCapacity" : 7500.0
  } ]
}

我们看到,每个Vehicle的子类都增加了一个@type属性,对于Car.class,其值为”car”,对于Truck.class其值为”truck”。这样反序列化的时候Jackson就知道怎么处理了。

除了用名字来区分类,我们当然可以用类的全限定名称。我们可以设置如下注解:

@JsonTypeInfo(
        use = JsonTypeInfo.Id.CLASS,
        include = JsonTypeInfo.As.PROPERTY,
        property = "@class")
@Data
@NoArgsConstructor
@AllArgsConstructor
public abstract class Vehicle {
    private String make;
    private String model;
}

使用use = JsonTypeInfo.Id.CLASS后,就不需要再给类起名字了。不过这对于非Java的客户端来说可能不太友好,而且类名通常很长。下面是测试的结果:

{
  "vehicles" : [ {
    "@class" : "com.github.fancyerii.learnjackson.a02.pojo2.Car",
    "make" : "BMW",
    "model" : "S500",
    "seatingCapacity" : 5,
    "topSpeed" : 250.0
  }, {
    "@class" : "com.github.fancyerii.learnjackson.a02.pojo2.Truck",
    "make" : "Isuzu",
    "model" : "NQR",
    "payloadCapacity" : 7500.0
  } ]
}

不过使用CLASS有个好处,如果我们增加一个新的子类,什么也不用做。但是如果使用Name,如果不增加@JsonSubTypes.Type,则会出错。比如我们定义了Car的子类Trailor,如果不做任何处理,则输出为:

{
  "vehicles" : [ {
    "@type" : "car",
    "make" : "BMW",
    "model" : "S500",
    "seatingCapacity" : 5,
    "topSpeed" : 250.0
  }, {
    "@type" : "truck",
    "make" : "Isuzu",
    "model" : "NQR",
    "payloadCapacity" : 7500.0
  }, {
    "@type" : "Trailer",
    "make" : "BMW",
    "model" : "T800",
    "seatingCapacity" : 10,
    "topSpeed" : 150.0,
    "trailerType" : "a"
  } ]
}

也就是没有定义名字的会默认使用类的名字(简短名字),在反序列化的时候Jackson找不到Trailer到底是哪个类,就会出现如下异常:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'Trailer' as a subtype of `com.github.fancyerii.learnjackson.a02.pojo2.Vehicle`: known type ids = [car, truck] (for POJO property 'vehicles')

也就是说,它不知道Trailer是哪个类,它只知道[car, truck]。

从子类里忽略一些字段

有的时候我们希望不要序列化/反序列父类的某些字段,我们可以通过如下几种方法实现。

注解

我们可以使用@JsonIgnore和@JsonIgnoreProperties两个注解来忽略某些字段。第一个是作用于类,而第二个作用于具体某个字段。我们通过例子来看它们的用法:

@Data
@NoArgsConstructor
@JsonIgnoreProperties({ "model", "seatingCapacity" })
public class Car extends Vehicle {
    private int seatingCapacity;
    @JsonIgnore
    private double topSpeed;

    public Car(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
        this.topSpeed = topSpeed;
    }

}

我们这里通过JsonIgnoreProperties忽略了集成来的”model”和它自己定义的seatingCapacity,同时通过JsonIgnore忽略了topSpeed。下面我们来看使用它的例子:

public class TestIgnore {
    public static void main(String[] args) throws JsonProcessingException {
        List<Vehicle> vehicleList = new ArrayList<>();
        vehicleList.add(new Car("BMW", "S500", 5, 250.0));
        vehicleList.add(new Truck("Isuzu", "NQR", 7500.0));

        Fleet fleet = new Fleet(vehicleList);

        ObjectMapper objectMapper = new ObjectMapper();
        ObjectWriter writer = objectMapper.writer(new DefaultPrettyPrinter());
        String json = writer.writeValueAsString(fleet);
        System.out.println(json);


        Fleet fleet2 = objectMapper.readValue(json, Fleet.class);
        json = writer.writeValueAsString(fleet2);
        System.out.println(json);
    }
}

输出为:

{
  "vehicles" : [ {
    "@type" : "car",
    "make" : "BMW"
  }, {
    "@type" : "truck",
    "make" : "Isuzu",
    "model" : "NQR",
    "payloadCapacity" : 7500.0
  } ]
}

我们看到,Car的实例只保存了make属性,而且反序列化的时候也不会有问题(不过其它字段都是空或者初始值)。

Mix-in

有的时候我们不想或者不能修改代码,比如某个类是第三方开发的。那么我们可以使用Mix-in的方法把这些注解混入到那个我们不能修改的类里面。我们通过一个例子来看看用法:

abstract class CarMixIn {
    @JsonIgnore
    public String make;
    @JsonIgnore
    public String topSpeed;
}

我们首先定义一个CarMixIn类,并且通过JsonIgnore忽略”make”和”topSpeed”两个字段,然后通过:

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.addMixIn(Car.class, CarMixIn.class);

也就是把CarMixIn的代码混入(覆盖)进Car,从而使得Car的make和topSpeed被忽略。最终运行的结果如下:

{
  "vehicles" : [ {
    "@type" : "car",
    "model" : "S500",
    "seatingCapacity" : 5
  }, {
    "@type" : "truck",
    "make" : "Isuzu",
    "model" : "NQR",
    "payloadCapacity" : 7500.0
  } ]
}

我们看到Car只有model和seatingCapacity字段,而make和topSpeed被忽略掉了。

使用注解的内省(Introspection)

实现属性忽略的最强大的终极的方法是通过集成JacksonAnnotationIntrospector自己编写代码来实现复杂的逻辑。

public class IgnoranceIntrospector extends JacksonAnnotationIntrospector {
    public boolean hasIgnoreMarker(AnnotatedMember m) {
        return m.getDeclaringClass() == Vehicle.class && m.getName() == "model"
                || m.getDeclaringClass() == Car.class
                || m.getName() == "towingCapacity"
                || super.hasIgnoreMarker(m);
    }
}

我们忽略掉了Vehicle的model字段,Car的全部字段,以及towingCapacity字段(不管它属于哪个类)。测试代码为:

public class TestAnnotationIntrospector {
    public static void main(String[] args) throws JsonProcessingException {
        List<Vehicle> vehicleList = new ArrayList<>();
        vehicleList.add(new Car("BMW", "S500", 5, 250.0));
        vehicleList.add(new Truck("Isuzu", "NQR", 7500.0));

        Fleet fleet = new Fleet(vehicleList);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setAnnotationIntrospector(new IgnoranceIntrospector());

        ObjectWriter writer = objectMapper.writer(new DefaultPrettyPrinter());
        String json = writer.writeValueAsString(fleet);
        System.out.println(json);

        objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        Fleet fleet2 = objectMapper.readValue(json, Fleet.class);
        json = writer.writeValueAsString(fleet2);
        System.out.println(json);
    }
}

输出的JSON为:

{
  "vehicles" : [ {
    "@type" : "com.github.fancyerii.learnjackson.a02.pojo4.Car",
    "make" : "BMW"
  }, {
    "@type" : "com.github.fancyerii.learnjackson.a02.pojo4.Truck",
    "make" : "Isuzu",
    "payloadCapacity" : 7500.0
  } ]
}

但是我们注意:IgnoranceIntrospector会更改输出的JSON,但是反序列化的时候它还是按照完整的类来处理,所以会抛出异常。如果我们还是想正常反序列化,则我们需要重新构造一个ObjectMapper(因为setAnnotationIntrospector会改变很多反序列化的行为)。

子类处理的场景

子类之间的转换

为了演示,我们把Car和Truck类特有的字段忽略掉,这样就可以避免不兼容的字段:

@Data
@NoArgsConstructor
public class Car extends Vehicle {
    @JsonIgnore
    private int seatingCapacity;
    @JsonIgnore
    private double topSpeed;

    public Car(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
        this.topSpeed = topSpeed;
    }

}

@Data
@NoArgsConstructor
public final class Truck extends Vehicle {
    @JsonIgnore
    private double payloadCapacity;

    public Truck(String make, String model, double payloadCapacity) {
        super(make, model);
        this.payloadCapacity = payloadCapacity;
    }
}

我们来测试一下:

public class TestSubTypeConversion {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();

        Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
        Truck truck = mapper.convertValue(car, Truck.class);
        System.out.println(mapper.writeValueAsString(truck));
    }
}

没有问题,不过注意:这里在Vehicle里不能定义多态的注解JsonTypeInfo,否则会出错。因为它会出错,因为它会检查@type和本身是否匹配。

没有构造函数

有的时候某个类没有空构造函数,那么反序列化就会失败,我们可以用@JsonCreator注解告诉Jackson用哪个构造函数构造对象:

@Data
public class Car extends Vehicle {
    private int seatingCapacity;
    private double topSpeed;

    @JsonCreator
    public Car(
            @JsonProperty("make") String make,
            @JsonProperty("model") String model,
            @JsonProperty("seating") int seatingCapacity,
            @JsonProperty("topSpeed") double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
        this.topSpeed = topSpeed;
    }
}

除了@JsonCreator,我们还得告诉Jackson非空构造函数的每一个参数对于JSON的哪个属性。有的读者可能奇怪,为什么Jackson不能自己对应上?因为函数的变量比如make在编译后默认是没有的(除非是调试版本)。所以我们需要显式的制定第一个参数对于Json的哪个属性。而且使用JsonProperty还有一个好处就是JSON属性名和参数可以不同。因为变量命名有一定的约束,比如不能以数字开始,但是JSON的属性并无此限制。所以如果JSON的属性名很诡异,我们也可以通过这种方法在Java里重命名一下。我们来测试一下:

public class TestNonEmptyConstructor {
    public static void main(String[] args) throws JsonProcessingException {
        List<Vehicle> vehicleList = new ArrayList<>();
        vehicleList.add(new Car("BMW", "S500", 5, 250.0));
        vehicleList.add(new Truck("Isuzu", "NQR", 7500.0));

        Fleet fleet = new Fleet(vehicleList);

        ObjectMapper objectMapper = new ObjectMapper();
        ObjectWriter writer = objectMapper.writer(new DefaultPrettyPrinter());
        String json = writer.writeValueAsString(fleet);
        System.out.println(json);


        Fleet fleet2 = objectMapper.readValue(json, Fleet.class);
        json = writer.writeValueAsString(fleet2);
        System.out.println(json);
    }
}

能正常序列化和反序列化,没有问题!