1. 概述
本文将探讨如何强制 Jackson 将JSON值反序列化为指定类型。
默认情况下,Jackson会根据目标字段的类型反序列化JSON值。但有时目标字段类型可能不够具体,这是为了允许多种类型的值。这种情况下,Jackson可能会选择指定类型的最近匹配子类型进行反序列化,可能导致意外结果。
我们将学习如何限制Jackson将JSON值反序列化为特定类型。
2. 代码示例准备
我们将定义一个包含多类型值字段的JSON结构,创建对应的Java类,并在特定情况下强制Jackson将值反序列化为指定类型。
2.1. 依赖项
首先在 pom.xml 中添加 Jackson Databind 依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
2.2. JSON结构
输入JSON结构如下:
{
"person": [
{
"key": "name",
"value": "John"
},
{
"key": "id",
"value": 25
}
]
}
这里 person 对象包含多个键值对属性,value 字段可以接受不同类型的值。
2.3. DTO
创建 DTO 类表示JSON结构:
public class PersonDTO {
private List<KeyValuePair> person;
// 构造方法、getter和setter
public static class KeyValuePair {
private String key;
private Object value;
// 构造方法、getter和setter
}
}
PersonDTO 包含表示人员信息的键值对列表,value 字段设为 Object 类型以支持多种值类型。
3. 默认反序列化行为
先看Jackson的默认反序列化机制。
3.1. 读取JSON
public PersonDTO readJson(String json) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(json, PersonDTO.class);
}
使用 ObjectMapper 将JSON字符串转换为 PersonDTO 对象。我们期望 id 值反序列化为 Long 类型。
3.2. 测试默认行为
测试方法并验证字段类型:
@Test
void givenJsonWithDifferentValueTypes_whenDeserialize_thenIntValue() throws JsonProcessingException {
String json = "{\"person\": [{\"key\": \"name\", \"value\": \"John\"}, {\"key\": \"id\", \"value\": 25}]}";
PersonDTO personDTO = readJson(json);
assertEquals(String.class, personDTO.getPerson().get(0).getValue().getClass());
assertEquals(Integer.class, personDTO.getPerson().get(1).getValue().getClass()); // 默认是Integer
}
测试通过,但有个坑:*Jackson将 id 值反序列化为 Integer 而非 Long,因为该值能被Integer容纳。*
接下来我们将修改这个默认行为。
4. 自定义反序列化为指定类型
最简单的方式是使用自定义反序列化器。
为 KeyValuePair 类的 value 字段创建自定义反序列化器:
public class ValueDeserializer extends JsonDeserializer<Object> {
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonToken currentToken = p.getCurrentToken();
if (currentToken == JsonToken.VALUE_NUMBER_INT) {
return p.getLongValue();
} else if (currentToken == JsonToken.VALUE_STRING) {
return p.getText();
}
return null;
}
}
从 JsonParser 获取当前token,判断是数字还是字符串。如果是数字则返回 Long 类型,字符串则返回 String 类型,强制将数值反序列化为 long。
然后在 KeyValuePair 类的 value 字段添加 @JsonDeserialize 注解:
public static class KeyValuePair {
private String key;
@JsonDeserialize(using = ValueDeserializer.class)
private Object value;
}
编写测试验证返回 Long 值:
@Test
void givenJsonWithDifferentValueTypes_whenDeserialize_thenLongValue() throws JsonProcessingException {
String json = "{\"person\": [{\"key\": \"name\", \"value\": \"John\"}, {\"key\": \"id\", \"value\": 25}]}";
PersonDTOWithCustomDeserializer personDTO = readJsonWithCustomDeserializer(json);
assertEquals(String.class, personDTO.getPerson().get(0).getValue().getClass());
assertEquals(Long.class, personDTO.getPerson().get(1).getValue().getClass());
}
5. 配置 ObjectMapper
上述方法适合特定字段的定制行为。但如果需要为整个类或多个类应用相同规则,可以配置 ObjectMapper 使用 USE_LONG_FOR_INTS 反序列化特性:
PersonDTO readJsonWithLongForInts(String json) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.USE_LONG_FOR_INTS);
return mapper.readValue(json, PersonDTO.class);
}
启用 USE_LONG_FOR_INTS 特性强制Jackson将所有整数值反序列化为 Long 类型。
测试配置效果:
@Test
void givenJsonWithDifferentValueTypes_whenDeserializeWithLongForInts_thenLongValue() throws JsonProcessingException {
String json = "{\"person\": [{\"key\": \"name\", \"value\": \"John\"}, {\"key\": \"id\", \"value\": 25}]}";
PersonDTO personDTO = readJsonWithLongForInts(json);
assertEquals(String.class, personDTO.getPerson().get(0).getValue().getClass());
assertEquals(Long.class, personDTO.getPerson().get(1).getValue().getClass());
}
测试通过,JSON中的所有整数值都被反序列化为 Long 类型。
6. 使用 @JsonTypeInfo
前两种方法会将 value 字段的所有整数值转为 Long。如果需要动态转换为特定类型,可以使用 @JsonTypeInfo 注解,但这要求输入JSON包含类型信息。
6.1. 在JSON中添加类型信息
修改JSON结构,为 value 字段添加类型信息:
{
"person": [
{
"key": "name",
"type": "string",
"value": "John"
},
{
"key": "id",
"type": "long",
"value": 25
},
{
"key": "age",
"type": "int",
"value": 30
}
]
}
添加 type 字段,并新增 Integer 类型的 age 字段以测试 int 和 long 值的反序列化。
6.2. 定制DTO
修改 KeyValuePair 类包含类型信息:
public class PersonDTOWithType {
private List<KeyValuePair> person;
public static class KeyValuePair {
private String key;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = String.class, name = "string"),
@JsonSubTypes.Type(value = Long.class, name = "long"),
@JsonSubTypes.Type(value = Integer.class, name = "int")
})
private Object value;
// 构造方法、getter和setter
}
}
使用 @JsonTypeInfo 指定 value 字段的类型信息,声明名为 "type" 的外部属性包含类型信息。
使用 @JsonSubTypes 定义所有可能的子类型:当 type 字段值为 "string" 时转换为 String 对象。
Jackson反序列化时会根据类型信息确定值的精确类型。
6.3. 测试
编写测试验证行为:
@Test
void givenJsonWithDifferentValueTypes_whenDeserializeWithTypeInfo_thenSuccess() throws JsonProcessingException {
String json = "{\"person\": [{\"key\": \"name\", \"type\": \"string\", \"value\": \"John\"}, {\"key\": \"id\", \"type\": \"long\", \"value\": 25}, {\"key\": \"age\", \"type\": \"int\", \"value\": 30}]}";
PersonDTOWithType personDTO = readJsonWithValueType(json);
assertEquals(String.class, personDTO.getPerson().get(0).getValue().getClass());
assertEquals(Long.class, personDTO.getPerson().get(1).getValue().getClass());
assertEquals(Integer.class, personDTO.getPerson().get(2).getValue().getClass());
}
测试通过,id 值被转换为 Long,age 值被转换为 Integer。
7. 总结
本文学习了强制Jackson反序列化为指定类型的多种方法:
- ✅ 自定义反序列化器
- ✅ 配置 ObjectMapper 的反序列化特性
- ✅ 使用 @JsonTypeInfo 指定类型信息
每种方法适用场景不同,根据实际需求选择即可。完整代码示例可在 GitHub 获取。