@requestbody泛型映射@JsonTypeInfo多态


原文链接:https://www.jianshu.com/p/a21f1633d79c

  • jackson允许配置多态类型处理,当进行反序列话时,JSON数据匹配的对象可能有多个子类型,为了正确的读取对象的类型,我们需要添加一些类型信息。可以通过下面几个注解来实现:

  • @JsonTypeInfo
    作用于类/接口,被用来开启多态类型处理,对基类/接口和子类/实现类都有效

  • @JsonTypeInfo(use = JsonTypeInfo.Id.NAME,include = JsonTypeInfo.As.PROPERTY,property = "name")
    这个注解有一些属性:

    • use:定义使用哪一种类型识别码,它有下面几个可选值:
      • JsonTypeInfo.Id.CLASS:使用完全限定类名做识别
      • JsonTypeInfo.Id.MINIMAL_CLASS:若基类和子类在同一包类,使用类名(忽略包名)作为识别码
      • JsonTypeInfo.Id.NAME:一个合乎逻辑的指定名称
      • JsonTypeInfo.Id.CUSTOM:自定义识别码,由@JsonTypeIdResolver对应,稍后解释
      • JsonTypeInfo.Id.NONE:不使用识别码
    • include(可选):指定识别码是如何被包含进去的,它有下面几个可选值:
      • JsonTypeInfo.As.PROPERTY:作为数据的兄弟属性
      • JsonTypeInfo.As.EXISTING_PROPERTY:作为POJO中已经存在的属性
      • JsonTypeInfo.As.EXTERNAL_PROPERTY:作为扩展属性
      • JsonTypeInfo.As.WRAPPER_OBJECT:作为一个包装的对象
      • JsonTypeInfo.As.WRAPPER_ARRAY:作为一个包装的数组
    • property(可选):制定识别码的属性名称
      此属性只有当:
      • useJsonTypeInfo.Id.CLASS(若不指定property则默认为@class)、JsonTypeInfo.Id.MINIMAL_CLASS(若不指定property则默认为@c)、JsonTypeInfo.Id.NAME(若不指定property默认为@type),
      • includeJsonTypeInfo.As.PROPERTYJsonTypeInfo.As.EXISTING_PROPERTYJsonTypeInfo.As.EXTERNAL_PROPERTY时才有效
    • defaultImpl(可选):如果类型识别码不存在或者无效,可以使用该属性来制定反序列化时使用的默认类型
    • visible(可选,默认为false):是否可见
      属性定义了类型标识符的值是否会通过JSON流成为反序列化器的一部分,默认为fale,也就是说,jackson会从JSON内容中处理和删除类型标识符再传递给JsonDeserializer。
  • @JsonSubTypes
    作用于类/接口,用来列出给定类的子类,只有当子类类型无法被检测到时才会使用它,一般是配合@JsonTypeInfo在基类上使用,比如:

    @JsonTypeName(value = "sub1")  
    

示例

  • 基类:
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * @author micocube
 * projectName: utils4j
 * packageName: jackson
 * email: ldscube@gmail.com
 * createTime: 2019-07-05 11:23
 * version: 0.1
 * description:
 */
public class Test {

    # 子类
    public static class TokenUser extends AbstractBaseEntity {

        private String token;


        public String getToken() {
            return token;
        }

        public TokenUser setToken(String token) {
            this.token = token;
            return this;
        }

        
  • 程序输出
{"type":"role","userName":"roleUserName","password":"rolePwd","roleName":"role"}
{"type":"token","userName":"tokenUserName","password":"tokenPassword","token":"token"}
RoleUser{roleName='role'}AbstractBaseEntity{userName='roleUserName', password='rolePwd'}
TokenUser{token='token'}AbstractBaseEntity{userName='tokenUserName', password='tokenPassword'}

参考资料

#Jackson对多态和多子类序列化的处理配置

十分钟学习Jackson多态处理

上案例:

{"name":"大象精"}
{"name":"孙悟空"}

嗯~ o( ̄▽ ̄)o,是可以序列化,但好像没有办法识别他们的类型啊。

  • @JsonTypeInfo注解开启多态处理

修改下代码:

{"@class":"com.tinylynn.springboot.json.type.Elephant","name":"大象精"}
{"@class":"com.tinylynn.springboot.json.type.Monkey","name":"孙悟空"}

看到不一样的东西了,Jackson帮我们添加了一个@class节点,值是完全限定类名

  • @JsonTypeInfo注解的说明

它有一些属性:

use(必选):定义使用哪一种类型识别码,可选值有多种:
JsonTypeInfo.Id.NONE:不使用识别码

{"@class":"com.tinylynn.springboot.json.type.Elephant","name":"大象精"}
{"@class":"com.tinylynn.springboot.json.type.Monkey","name":"孙悟空"}

JsonTypeInfo.Id.MINIMAL_CLASS:表示具有最小路径的Java类名称用作识别,识别码名称@c(慎用)

{"@type":"Elephant","name":"大象精"}
{"@type":"Monkey","name":"孙悟空"}

JsonTypeInfo.Id.CUSTOM:自定义识别码,需结合property属性和@JsonTypeIdResolver注释,后面给出案例。

  • include(可选):设置识别码包含在哪里。

JsonTypeInfo.As.PROPERTY:作为POJO的属性出现(默认)
JsonTypeInfo.As.WRAPPER_OBJECT:作为一个包装的对象

{"Elephant":{"name":"大象精"}}
{"Monkey":{"name":"孙悟空"}}

JsonTypeInfo.As.WRAPPER_ARRAY:作为一个包装的数组

public class Zoo {
    @JsonTypeInfo(use=Id.NAME,include=As.EXTERNAL_PROPERTY)
    private Animal animal;

    //动物园名称
    private String name;
    //忽略getter和setter
}

Zoo zoo = new Zoo();
zoo.setName("只有一只大象的动物园");
zoo.setAnimal(elephant);
System.out.println(objectMapper.writeValueAsString(zoo));

输出:

@JsonTypeInfo(
    use=Id.NAME,
    include=As.EXISTING_PROPERTY,
    property="type" //设置识别码名称为type,跟字段type名称一样。
)
@JsonSubTypes({ //设置对应子类的识别码值
    @Type(value = Monkey.class, name = "猴子") ,
    @Type(value = Elephant.class, name = "大象") 
})
public abstract class Animal {
    private String type; //新增类型
    private String name;
    //忽略getter和setter
}

ObjectMapper objectMapper = new ObjectMapper();
        
Elephant elephant = new Elephant();
elephant.setName("孤单的大象");
String elephantJson = objectMapper.writeValueAsString(elephant); System.out.println(elephantJson);
        
Elephant anotherElephant = new Elephant();
anotherElephant.setName("另一头孤单的大象");
anotherElephant.setType("大象");
String anotherElephantJson = objectMapper.writeValueAsString(anotherElephant);
System.out.println(anotherElephantJson);

输出:

String deElephant = "{\"type\":\"大象\",\"name\":\"另一头孤单的大象\"}";
Animal elephant = objectMapper.readValue(deElephant, Animal.class);
System.out.println(elephant instanceof Elephant); //true

在反序列化时候,type的值被认为是识别码,如果type的值不是[大象,猴子]其中之一,则程序会抛出异常。

property(可选):设置识别码是名称,在include=JsonTypeInfo.As.PROPERTY或use=JsonTypeInfo.Id.CUSTOM生效。其他情况使用默认的识别码名称。
注意:include=JsonTypeInfo.As.PROPERTY和property同时存在有个问题,如果POJO具有相同名称的属性,会出现两个..

上案例:

{"type":"大象","type":"猴子","name":"我是什么动物"}

好神奇,两个type。其实好理解啦,include=As.PROPERTY告诉Jackson 识别码是作为POJO的属性出现,而同时你告诉Jackson识别码名称为type,Jackson才不管你POJO是不是已经有包含type属性,都给你输出。如果没有设置property属性,则使用默认的识别码名称,就是@type。
比这个更神奇的,上案例:

[{"type":"我是大象","name":"安安"},{"type":"我是猴子","name":"宁宁"}]

没有识别码,被吃掉了!!!什么原因不知道,记住这个坑就好了!!

visible(可选):定义识别码在反序列化时是否保留(不管false或true都不影响序列化)。默认是false,表示Jackson可以将识别码识别为类型后就删除。
看不懂,上案例:

{"type":"大象","type":null,"name":"安安"}
true //说明识别码是有效的
null //说明Jackson处理完识别码就删除了

将visible改为true再执行一遍:

输出:

{"type":"国宝熊猫","type":null,"name":"贝贝"}