【后端 老中医】利用@JsonTypeInfo实现JSON的多态序列化和反序列化

5,953 阅读3分钟

摘要

Jackson中利用@JsonTypeInfo可以实现多态的序列化和反序列化,从而达到灵活的编码目的。

最佳实践

JsonTypeInfo.png

@Data
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true,
        defaultImpl = Action.class, include = JsonTypeInfo.As.EXISTING_PROPERTY)
@JsonSubTypes({
        @JsonSubTypes.Type(value = CallPhoneAction.class, name = "CALL_PHONE"),
        @JsonSubTypes.Type(value = SendEmailAction.class, name = "SEND_EMAIL"),
})
public class Action {
    /**
     * 动作类型
     */
    private String type;
}

@Data
@EqualsAndHashCode(callSuper = true)
public class CallPhoneAction extends Action {
    /**
     * 电话号码
     */
    private String phoneNumber;

    /**
     * 语音内容
     */
    private String content;
}

@Data
@EqualsAndHashCode(callSuper = true)
public class SendEmailAction extends Action {
    /**
     * 对方的邮箱
     */
    private String toEmailAddress;

    /**
     * 抄送的邮箱
     */
    private String ccEmailAddress;

    /**
     * 邮件标题
     */
    private String title;

    /**
     * 邮件内容
     */
    private String content;
}

测试代码

@Slf4j
public class RunningApplicationTest {
    static private ObjectMapper objectMapper = new ObjectMapper();

    @Test
    public void test() throws JsonProcessingException {
        String SendEmailJson = "" +
                "{\n" +
                "    \"type\":           \"SEND_EMAIL\",\n" +
                "    \"toEmailAddress\": \"receive@example.com\",\n" +
                "    \"title\":          \"这是邮件标题\",\n" +
                "    \"content\":        \"这是邮件内容\"\n" +
                "}";
        Action action = objectMapper.readValue(SendEmailJson, Action.class);

        log.info("the claas name of action is {}", action.getClass().getSimpleName());
        log.info("{}", action);
    }
}

输出

11:52:23.199 [main] INFO org.example.RunningApplicationTest - the claas name of action is SendEmailAction
11:52:23.203 [main] INFO org.example.RunningApplicationTest - SendEmailAction(toEmailAddress=receive@example.com, ccEmailAddress=null, title=这是邮件标题, content=这是邮件内容)

用于生产中的demo

@RestController
@RequestMapping("action/")
public class Controller {
    @PostMapping
    public Action submit(@RequestBody Action action) {
        if (action instanceof CallPhoneAction) {
            // 执行打电话动作
        } else if (action instanceof SendEmailAction) {
            // 执行发邮件动作
        } else {
            //输入的type错误,导致 action 没有被反序列化成子类
            throw new RuntimeException("Type unrecognized. " + action);
        }

        return action;
    }
}

@JsonTypeInfo 解释

@JsonTypInfo

用于配置JSON 序列化和反序列化过程中使用哪种类型信息。该注解是实现多态类型的必要注解。

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true,
        defaultImpl = Action.class, include = JsonTypeInfo.As.EXISTING_PROPERTY)
  1. use:用于指定用哪种类型的元信息来序列化和反序列化。有下面几种选择
    • JsonTypeInfo.Id.NONE: JSON字符串中不包含显式的元信息用于识别类型。开发人员使用其他的标识手段来确定类型。一般用不到。
    • JsonTypeInfo.Id.CLASS: 使用全限定名来识别类型。好处是不用写@JsonSubTypes,缺点是JSON强依赖代码,一旦代码改动,JSON也要跟着变化。
    @Data
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "type", visible = true,
            defaultImpl = Action.class, include = JsonTypeInfo.As.EXISTING_PROPERTY)
    public class Action {
        /**
         * 动作类型
         */
        private String type;
    }
    @Slf4j
    public class RunningApplicationTest {
        @Test
        public void test2() throws JsonProcessingException {
            String json = "{\n" +
                    "    \"type\":      \"org.example.dao.SendEmailAction\",\n" +
                    "    \"toEmailAddress\": \"receive@example.com\",\n" +
                    "    \"ccEmailAddress\": null,\n" +
                    "    \"title\":          \"这是邮件标题\",\n" +
                    "    \"content\":        \"这是邮件内容\"\n" +
                    "}";
            Action action = objectMapper.readValue(json, Action.class);
    
            log.info("the claas name of action is {}", action.getClass().getSimpleName());
            log.info("{}", action);
        }
    }
    
    输出为
    12:13:33.237 [main] INFO org.example.RunningApplicationTest - the claas name of action is SendEmailAction
    12:13:33.241 [main] INFO org.example.RunningApplicationTest - SendEmailAction(toEmailAddress=receive@example.com, ccEmailAddress=null, title=这是邮件标题, content=这是邮件内容)
    
    • JsonTypeInfo.Id.MINIMAL_CLASS:与JsonTypeInfo.Id.CLASS类似。区别展示可唯一定位一个类的最小路径。例如这里SendEmailAction的最小路径是"type":".SendEmailAction"。其中"."是不可获取的,表示相对路径的起点。
    • JsonTypeInfo.Id.NAME:逻辑名称,配合@JsonSubTypes可以实现自定义标识。在本例中笔者自定义标识符为"CALL_PHONE""SEND_EMAIL"
    • JsonTypeInfo.Id.CUSTOM: 表示类型化机制使用自定义处理,需要自定义指向Jackson中的类型化机制。一般不用。
  2. property:指定哪个字段用于标识类型。本例中,笔者使用"type"字段作为类型标识字段。
  3. visible:当为true时,property中用到的字段将参与序列化和反序列化中;为false时不参与序列化和反序列化。
  4. defaultImpl:指定默认类,用于Jackson不能反序列化为任意一个子类时使用。可以填写Void,将反序列化为null。默认值为JsonTypeInfo.class,抛出com.fasterxml.jackson.databind.exc.InvalidTypeIdException异常。
  5. include:指定要如何处理类型标识。
    • JsonTypeInfo.As.PROPERTY:序列化时,类型标识字段作为json的一个字段。此时无论json中是否已经存在该字段。因此 visible = trueinclude = JsonTypeInfo.As.EXISTING_PROPERTY搭配时,会导致序列化后json有两个重复的字段,本例中是重复的 type=SEND_EMAIL
    • JsonTypeInfo.As.WRAPPER_OBJECT: 序列化时,在json中类型标识字段会使用大括号包裹起来
    • JsonTypeInfo.As.WRAPPER_ARRAY: 序列化时,在json中类型标识字段会使用中括号包裹起来
    • JsonTypeInfo.As.EXTERNAL_PROPERTY: 序列化时,在json中类型标识字段和序列化的类同级
    • JsonTypeInfo.As.EXISTING_PROPERTY: 序列化时,如果json中已经有同名字段,那么类型标识字段不序列化。