Jackson如何反序列化Java14中的record类型?

Java14近日发布,其中引入了新的record类型,虽然这是个预览特性,但是也不妨碍我们尝试下。

record类型

record类型和普通的类相比,有几个特点

  1. 每个字段都是private final的,类本身是final的
  2. 类只有一个所有传递所有参数的构造函数
  3. 每个字段会自动会有一个get方法,但是名字和字段名一致(没有get前缀)
  4. 可以给字段添加注解,支持FIELD、METHOD和PARAMETER(如果一个注解是FIELD、METHOD和PARAMETER的,那么注解会在字段、get方法和构造函数参数上同时出现)
  5. toString、equals、hashCode方法都是自带的

这几个特点决定了这个非常适合做DO(Model),DTO(比如RPC中定义的各种entity,http返回的json)。

由于业务中经常用到jackson来序列化反序列化对象,今天我们就来试一试jackson对record类型的支持。

如何使用jackson反序列化record对象

如果我们按照原来的方式来反序列化record(如果字段上没有@JsonProperty注解的话),会直接报错:InvalidDefinitionException: Cannot construct instance of Person (no Creators, like default construct, exist)。这是因为jackson没法找到构造函数和字段的映射,所以我们自己指定默认的映射:

// from https://gist.github.com/youribonnaffe/03176be516c0ed06828ccc7d6c1724ce
JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
    @Override
    public String findImplicitPropertyName(AnnotatedMember m) {
        if (m.getDeclaringClass().isRecord()) {
            if (m instanceof AnnotatedParameter parameter) {
                return m.getDeclaringClass().getRecordComponents()[parameter.getIndex()].getName();
            }
        }
        return super.findImplicitPropertyName(m);
    }
};
objectMapper.setAnnotationIntrospector(implicitRecordAI);

可以看到,jdk提供了一系列方法来处理record类型,比如getRecordComponents来获取每个字段(包含了字段在构造函数中的位置)。

这样我们就能正常反序列化了:

@JsonIgnoreProperties(ignoreUnknown = true)
public record Person(
        Integer id,
        String name
) {
    public static void main(String[] args) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
            @Override
            public String findImplicitPropertyName(AnnotatedMember m) {
                if (m.getDeclaringClass().isRecord()) {
                    if (m instanceof AnnotatedParameter parameter) {
                        return m.getDeclaringClass().getRecordComponents()[parameter.getIndex()].getName();
                    }
                }
                return super.findImplicitPropertyName(m);
            }
        };
        objectMapper.setAnnotationIntrospector(implicitRecordAI);
        String jsonStr = """
                {
                  "id":1,
                  "name":"name1",
                  "alias": "alias1,alias2"
                }
                """;
        Person person = objectMapper.readValue(jsonStr, Person.class);
        System.out.println(person);
        // Person[id=1, name=name1]
    }
}

但是日常开发中,我们又会遇到不规范的json,比如Person对象有一个alias字段,是List<String>类型,但是数据源可能就是alias1,alias2这样的,所以我们得让record支持这样的玩法。

原来我们是直接自定义JsonSetter的:

private List<String> alias;

@JsonSetter("alias")
public void setAlias(String aliasStr) {
    alias = Arrays.asList(aliasStr.split(","));
}

但是用了record后,由于alias是final的,这样的方法没法用了。

所以找了下,可以使用JsonDeserialize注解,给某个具体的字段指定converter,例子如下:

@JsonIgnoreProperties(ignoreUnknown = true)
public record Person(
        Integer id,
        String name,
        // 如果字段有JsonDeserialize注解,那么一定要加上JsonProperty注解指定名字
        @JsonProperty(value = "alias", access = JsonProperty.Access.WRITE_ONLY)
        @JsonDeserialize(converter = AliasConverter.class)
        List<String>alias
) {
    public static void main(String[] args) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
            // ......
        };
        objectMapper.setAnnotationIntrospector(implicitRecordAI);
        String jsonStr = """
                {
                  "id":1,
                  "name":"name1",
                  "alias": "alias1,alias2"
                }
                """;
        Person person = objectMapper.readValue(jsonStr, Person.class);
        System.out.println(person);
        // Person[id=1, name=name1, alias=[alias1, alias2]]
    }

    /**
     * 用于将逗号分割的String转成record中的List
     */
    static class AliasConverter extends StdConverter<String, List<String>> {
        @Override
        public List<String> convert(String value) {
            return Arrays.stream(value.split(",")).collect(Collectors.toList());
        }
    }
}

至此,反序列化上就没什么大坑了。

如何使用jackson序列化record对象

刚刚我们定制了alias字段的反序列化逻辑,现在我们要序列化,所以也要定制alias字段的序列化逻辑;然后我们增加了password字段,不要将这个字段序列化出来:

@JsonIgnoreProperties(ignoreUnknown = true)
public record Person(
        Integer id,
        String name,
        @JsonProperty(value = "alias", access = JsonProperty.Access.WRITE_ONLY)
        @JsonDeserialize(converter = AliasConverter.class)
        List<String>alias,
        @JsonProperty(value = "password", access = JsonProperty.Access.WRITE_ONLY)
        String password
) {
    @JsonGetter("alias")
    public String jsonGetAlias() {
        return String.join(",", alias);
    }

    public static void main(String[] args) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
            @Override
            public String findImplicitPropertyName(AnnotatedMember m) {
                if (m.getDeclaringClass().isRecord()) {
                    if (m instanceof AnnotatedParameter parameter) {
                        return m.getDeclaringClass().getRecordComponents()[parameter.getIndex()].getName();
                    }
                    // 反序列化时是检查member,所以加特殊处理
                    if (m instanceof AnnotatedMember member) {
                        for (RecordComponent recordComponent : m.getDeclaringClass().getRecordComponents()) {
                            if (recordComponent.getName().equals(member.getName())) {
                                return member.getName();
                            }
                        }
                    }
                }
                return super.findImplicitPropertyName(m);
            }
        };
        objectMapper.setAnnotationIntrospector(implicitRecordAI);
        String jsonStr = """
                {
                  "id":1,
                  "name":"name1",
                  "alias": "alias1,alias2",
                  "password": "password1"
                }
                """;
        Person person = objectMapper.readValue(jsonStr, Person.class);
        System.out.println(person);
        // erson[id=1, name=name1, alias=[alias1, alias2], password=password1]
        String jsonOut = objectMapper.writeValueAsString(person);
        System.out.println(jsonOut);
        // {"alias":"alias1,alias2","id":1,"name":"name1"}
    }

    /**
     * 将String方式的alias转成List<String>
     */
    static class AliasConverter extends StdConverter<String, List<String>> {
        @Override
        public List<String> convert(String value) {
            return Arrays.stream(value.split(",")).collect(Collectors.toList());
        }
    }
}

这里面有几个点要注意:

  1. access = JsonProperty.Access.WRITE_ONLY 是指反序列会写入,序列化时不会读取, 这也保证了password不会被输出。
  2. 反序列化时是检查member,所以必须加m instanceof AnnotatedMember member
  3. jsonGetAlias方法带上了@JsonGetter("alias"),alias的序列化就会走这个方法

最终的示例代码

// 最终的示例代码
@JsonIgnoreProperties(ignoreUnknown = true)
public record Person(
        Integer id,
        String name,
        // if this field have custom converter, JsonProperty is required
        @JsonProperty(value = "alias", access = JsonProperty.Access.WRITE_ONLY)
        @JsonDeserialize(converter = AliasConverter.class)
        List<String>alias,
        @JsonProperty(value = "password", access = JsonProperty.Access.WRITE_ONLY)
        String password
) {
    @JsonGetter("alias")
    public String jsonGetAlias() {
        return String.join(",", alias);
    }

    public static void main(String[] args) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        // get default propertyName from record
        // thanks youribonnaffe
        JacksonAnnotationIntrospector implicitRecordAI = new JacksonAnnotationIntrospector() {
            @Override
            public String findImplicitPropertyName(AnnotatedMember m) {
                if (m.getDeclaringClass().isRecord()) {
                    if (m instanceof AnnotatedParameter parameter) {
                        return m.getDeclaringClass().getRecordComponents()[parameter.getIndex()].getName();
                    }
                    if (m instanceof AnnotatedMember member) {
                        for (RecordComponent recordComponent : m.getDeclaringClass().getRecordComponents()) {
                            if (recordComponent.getName().equals(member.getName())) {
                                return member.getName();
                            }
                        }
                    }
                }
                return super.findImplicitPropertyName(m);
            }
        };
        objectMapper.setAnnotationIntrospector(implicitRecordAI);
        String jsonStr = """
                {
                  "id":1,
                  "name":"name1",
                  "alias": "alias1,alias2",
                  "password": "password1"
                }
                """;
        Person person = objectMapper.readValue(jsonStr, Person.class);
        System.out.println(person);
        // Person[id=1, name=name1, alias=[alias1, alias2], password=password1]
        String jsonOut = objectMapper.writeValueAsString(person);
        System.out.println(jsonOut);
        // {"alias":"alias1,alias2","id":1,"name":"name1"}
    }

    /**
     * convert String to List, just for field alias
     * <p>
     * this is class-bound logic, so keep it internal
     */
    static class AliasConverter extends StdConverter<String, List<String>> {
        @Override
        public List<String> convert(String value) {
            return Arrays.stream(value.split(",")).collect(Collectors.toList());
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.