1

Jackson学习笔记(二)

 2 years ago
source link: http://fancyerii.github.io/2022/03/21/jackson2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文介绍怎么在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);
    }
}

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



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK