Post

Jackson Json序列化反序列化的两个坑

Jackson is a suite of data-processing tools for Java (and the JVM platform)

Jackson最常用的Json序列化功能,引入如下的包即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<properties>
    ...
    <!-- Use the latest version whenever possible. -->
    <jackson.version>2.17.1</jackson.version>
    ...
</properties>

<dependencies>
    ...
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    ...
</dependencies>

Json序列化的坑

首先来看一个Json序列化异常。 JsonUtil工具类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;

public class JsonUtil {

    private static final ObjectMapper OBJECT_MAPPER = buildLowerCamelCase();

    private static ObjectMapper buildLowerCamelCase() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE);
        return objectMapper;
    }

    public static String json(Object obj) {
        try {
            return OBJECT_MAPPER.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

}

另外有一个实体类CodeMonkey:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import lombok.Data;
import java.util.Objects;

@Data
public class CodeMonkey {

    private Long id;

    private String name;

    public boolean isJavaer () {
        // mock throw exception
        throw new IllegalArgumentException("should be javaer");
    }
}

我们对改实体对象进行Json序列化:

1
2
3
4
5
6
7
8
9
10
11
12
public class JacksonTestApplication {

    public static void main(String[] args) {

        CodeMonkey codeMonkey = new CodeMonkey();
        codeMonkey.setId(1L);
        codeMonkey.setName("not bty");

        log.info("hello , {}", JsonUtil.json(codeMonkey));
    }

}

运行发现报错:

1
2
3
4
5
6
7
Exception in thread "main" java.lang.RuntimeException: com.fasterxml.jackson.databind.JsonMappingException: should be javaer (through reference chain: io.github.bty834.jacksontest.CodeMonkey["javaer"])
	at io.github.bty834.jacksontest.util.JsonUtil.json(JsonUtil.java:22)
	at io.github.bty834.jacksontest.JacksonTestApplication.main(JacksonTestApplication.java:16)
Caused by: com.fasterxml.jackson.databind.JsonMappingException: should be javaer (through reference chain: io.github.bty834.jacksontest.CodeMonkey["javaer"])
	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:392)
	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:351)
	...

可以看到Jackson将isJavaer方法当作字段来序列化,但isJavaer其实是不需要序列化的, 可以在方法上添加 com.fasterxml.jackson.annotation.JsonIgnore 注解忽略该方法。

经过调试,Jackson使用com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector#collectAll方法获取某个类型需要 序列化的属性信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class POJOPropertiesCollector {
    ...
    protected void collectAll() {
        LinkedHashMap<String, POJOPropertyBuilder> props = new LinkedHashMap<String, POJOPropertyBuilder>();

          
        // 最终调用 java.lang.Class#getDeclaredFields 获取字段
        // final字段和transient字段可配置,默认都序列化
        _addFields(props); 
        // 最终调用 java.lang.Class#getDeclaredMethods 
        // 获取get/set/is开头的方法(且方法入参分别为0或1,any getter这里不讨论)
        _addMethods(props);
        ...
        // 去掉不需要的属性, 如isJavaer方法产生的javaer prop
        _removeUnwantedProperties(props);
        ...
    }
    ...
}

Json反序列化的坑

回到CodeMonkey类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Data
public class CodeMonkey {

    private Long id;

    private String name;

    private Language language;

    @JsonIgnore
    public boolean isJavaer () {
        if (Objects.equals(this.name, "bty")) {
            return true;
        }
        throw new IllegalArgumentException("should be javaer");
    }
}

@Getter
@AllArgsConstructor
public enum Language {

    JAVA(1, "Java"),
    C_PLUS_PLUS(2, "C++"),
    GO(10, "Go");

    private final int code;
    private final String name;
}

我们选取两个json字符串模拟反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
public class JacksonTestApplication {
    public static void main(String[] args) {
        // language取1
        String jsonStr1 = "{\"id\":1,\"name\":\"bty\",\"language\": 1}";
        CodeMonkey codeMonkey1 = JsonUtil.fromJson(jsonStr1, CodeMonkey.class);
        System.out.println(codeMonkey1); // CodeMonkey(id=1, name=bty, language=C_PLUS_PLUS)

        // language取JAVA
        String jsonStr2 = "{\"id\":1,\"name\":\"bty\",\"language\": \"JAVA\"}";
        CodeMonkey codeMonkey2 = JsonUtil.fromJson(jsonStr2, CodeMonkey.class);
        System.out.println(codeMonkey2); // CodeMonkey(id=1, name=bty, language=JAVA)
    }
}

为啥我们language传1时,反序列化为C_PLUS_PLUS枚举了呢?C_PLUS_PLUScode字段也不是1啊

在不添加jackson额外注解时,枚举序列化后是枚举值即大写英文字母,而反序列化会经过多个流程。

具体来说,枚举的反序列化默认通过com.fasterxml.jackson.databind.deser.std.EnumDeserializer#deserialize完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class EnumDeserializer
    extends StdScalarDeserializer<Object>
    implements ContextualDeserializer
{
    ...
    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
    {
        
        if (p.hasToken(JsonToken.VALUE_STRING)) {
            // 1. 如果传的是字符串,则解析字符串
            return _fromString(p, ctxt, p.getText());
        }

        // 如果是int能表示的数字(包含能转为int的long类型)
        if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { 
            
            if (_isFromIntValue) {  // 判断根据@JsonValue的值来解析

                // 将数字转换为字符串根据@JsonValue注解的字段或方法解析
                return _fromString(p, ctxt, p.getText());
            }
            
            // 根据反序列化配置的CoercionAction取值或报错
            // CoercionAction 分为 Fail , TryConvert , AsNull , AsEmpty
            // 枚举这里为TryConvert,具体逻辑参考:com.fasterxml.jackson.databind.cfg.CoercionConfigs#findCoercion
            // TryConvert会使用枚举值在枚举中的index来匹配这里的int,很坑!
            return _fromInteger(p, ctxt, p.getIntValue());
        }
        
        // 解析嵌套json对象
        if (p.isExpectedStartObjectToken()) {
            return _fromString(p, ctxt,
                    ctxt.extractScalarFromObject(p, this, _valueClass));
        }
        // 解析嵌套json数组
        return _deserializeOther(p, ctxt);
    }
    ...
}

如果规避以上的坑呢?

@JsonValue

code字段添加@JsonValue,则反序列化只能根据code值(int或int的字符串都行)进行,除了会影响反序列化,序列化时会将该枚举写成其code

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@AllArgsConstructor
public enum Language {

    JAVA(1L, "Java"),
    C_PLUS_PLUS(2L, "C++"),
    GO(10L, "Go");

    @JsonValue
    private final long code;
    private final String name;
}

@JsonCreator

  1. 如果我既想要根据code字段也想要根据Enum.name()来反序列化呢?自定义反序列化方法并注解@JsonCreator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Getter
@AllArgsConstructor
public enum Language {

    JAVA(1L, "Java"),
    C_PLUS_PLUS(2L, "C++"),
    GO(10L, "Go");

    @JsonValue
    private final long code;
    private final String name;

    @JsonCreator
    public static Language parse(String val) {
        for (Language value : Language.values()) {
            // 先根据Enum.name()判断
            if (value.name().equals(val)) {
                return value;
            }
            // 再根据code字段判断
            if (Objects.equals(String.valueOf(value.code), val)) {
                return value;
            }
        }
        return null;
    }
}

DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS

可以打开反序列化配置objectMapper.enable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS); 来避免int匹配不到时根据index来匹配,会直接报错。但并不影响@JsonValue@JsonCreator的使用。

This post is licensed under CC BY 4.0 by the author.