
引言与问题背景
在与外部服务进行数据交互时,我们经常会遇到json数据结构不完全固定的情况。尤其当json的顶级属性键是动态生成或随机变化的,例如"random1"、"nextrandom500"等,而其对应的值结构却是固定的对象时,使用jackson进行标准pojo(plain old java object)映射会遇到挑战。jackson默认期望json的键与pojo的属性名严格对应,一旦遇到未知属性,便会抛出com.fasterxml.jackson.databind.exc.unrecognizedpropertyexception异常。
例如,以下JSON结构中,random1、nextRandom500、random100500都是动态键:
{
"random1" : {
"name" : "john",
"lastName" : "johnson"
},
"nextRandom500" : {
"name" : "jack",
"lastName" : "jackson"
},
"random100500" : {
"name" : "jack",
"lastName" : "johnson"
}
}当尝试将上述JSON直接反序列化到一个包含Map
问题分析:为何会抛出UnrecognizedPropertyException?
Jackson在进行对象反序列化时,会尝试将JSON中的每个键映射到目标POJO类中的一个属性。如果JSON中存在POJO类中未定义的顶级键,且没有特殊配置(如@JsonIgnoreProperties(ignoreUnknown = true)),Jackson就会认为这是一个“无法识别的属性”,并抛出UnrecognizedPropertyException。
在上述示例中,如果我们的UserResponse POJO定义如下:
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Map;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder(toBuilder=true)
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserResponse {
private Map users; // Jackson会寻找名为"users"的键
@Data
@SuperBuilder(toBuilder=true)
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class User {
private String name;
private String lastName;
}
} 而实际的JSON数据是:
{
"random1" : { /* ... */ },
"nextRandom500" : { /* ... */ }
}Jackson会尝试将"random1"映射到UserResponse的某个属性,但UserResponse中只有一个users属性。由于"random1"与"users"不匹配,Jackson便会抛出异常。
解决方案一:直接映射到Map结构
当JSON的顶级键是动态的,但其值结构是固定的对象时,最直接有效的方法是将整个JSON反序列化为一个Map
1. POJO定义
首先,我们需要定义动态键所对应的值对象User:
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder(toBuilder=true)
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
private String name;
private String lastName;
}注意:这里不再需要一个外部的UserResponse类来包装这个Map,因为我们将直接把整个JSON看作一个Map。
2. 反序列化
使用ObjectMapper结合TypeReference将JSON字符串反序列化为Map
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.IOException;
import java.util.Map;
public class JsonDeserializationExample {
public static void main(String[] args) throws IOException {
String jsonString = "{\n" +
" \"random1\" : {\n" +
" \"name\" : \"john\",\n" +
" \"lastName\" : \"johnson\"\n" +
" },\n" +
" \"nextRandom500\" : {\n" +
" \"name\" : \"jack\",\n" +
" \"lastName\" : \"jackson\"\n" +
" },\n" +
" \"random100500\" : {\n" +
" \"name\" : \"jack\",\n" +
" \"lastName\" : \"johnson\"\n" +
" } \n" +
"}";
ObjectMapper objectMapper = new ObjectMapper();
// 配置以美化输出,非必需
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
// 使用TypeReference进行反序列化
Map usersMap = objectMapper.readValue(jsonString, new TypeReference 解释: new TypeReference
3. 序列化
将Map
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class JsonSerializationExample {
public static void main(String[] args) throws IOException {
// 创建一个Map对象
Map usersMap = new HashMap<>();
usersMap.put("randomA", new User("Alice", "Smith"));
usersMap.put("randomB", new User("Bob", "Johnson"));
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT); // 美化输出
// 序列化Map到JSON字符串
String serializedJson = objectMapper.writeValueAsString(usersMap);
System.out.println("序列化后的JSON:");
System.out.println(serializedJson);
}
} 解决方案二:使用包装POJO(适用于特定JSON结构)
如果JSON的结构并非直接由动态键构成,而是包含一个固定的顶级键,该键的值是一个由动态键组成的Map,那么使用一个包装POJO是更合适的选择。
例如,如果你的JSON数据实际上是这样的:
{
"users": {
"random1" : {
"name" : "john",
"lastName" : "johnson"
},
"nextRandom500" : {
"name" : "jack",
"lastName" : "jackson"
},
"random100500" : {
"name" : "jack",
"lastName" : "johnson"
}
}
}在这种情况下,"users"是一个固定的顶级键,其值是一个Map
1. POJO定义
此时,你最初定义的UserResponse POJO是完全适用的:
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Map;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder(toBuilder=true)
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserResponse {
private Map users; // Jackson会寻找名为"users"的键
@Data
@SuperBuilder(toBuilder=true)
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class User {
private String name;
private String lastName;
}
} 2. 反序列化与序列化
直接使用ObjectMapper对UserResponse.class进行反序列化和序列化:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class WrappedPojoExample {
public static void main(String[] args) throws IOException {
String wrappedJsonString = "{\n" +
" \"users\": {\n" +
" \"random1\" : {\n" +
" \"name\" : \"john\",\n" +
" \"lastName\" : \"johnson\"\n" +
" },\n" +
" \"nextRandom500\" : {\n" +
" \"name\" : \"jack\",\n" +
" \"lastName\" : \"jackson\"\n" +
" },\n" +
" \"random100500\" : {\n" +
" \"name\" : \"jack\",\n" +
" \"lastName\" : \"johnson\"\n" +
" }\n" +
" }\n" +
"}";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
// 反序列化
UserResponse userResponse = objectMapper.readValue(wrappedJsonString, UserResponse.class);
System.out.println("反序列化成功,UserResponse内容:");
userResponse.getUsers().forEach((key, user) ->
System.out.println(" Key: " + key + ", User: " + user.getName() + " " + user.getLastName())
);
// 序列化
UserResponse newResponse = new UserResponse();
Map newUsers = new HashMap<>();
newUsers.put("newRandomX", new User("Charlie", "Brown"));
newResponse.setUsers(newUsers);
String serializedWrappedJson = objectMapper.writeValueAsString(newResponse);
System.out.println("\n序列化后的JSON:");
System.out.println(serializedWrappedJson);
}
} 总结与注意事项
-
理解JSON结构是关键: 在选择反序列化策略之前,务必清晰地了解你所处理的JSON数据的确切结构。
- 如果整个JSON的顶级键都是动态的,且其值是结构化对象,请使用解决方案一(直接映射到Map)。
- 如果JSON包含一个固定的顶级键,该键的值是一个动态键的Map,请使用解决方案二(使用包装POJO)。
-
TypeReference的用途: TypeReference是Jackson处理泛型类型(如List
、Map )时获取完整类型信息的强大工具,尤其在编译时无法确定具体泛型参数时非常有用。 - Lombok简化POJO: 示例中使用了Lombok注解(@Data, @SuperBuilder, @NoArgsConstructor)来简化POJO的编写,减少了样板代码。在实际项目中,可以根据需要选择使用。
- @JsonInclude(JsonInclude.Include.NON_NULL): 这个Jackson注解在序列化时会忽略值为null的属性,使得生成的JSON更加简洁。
- 异常处理: 在实际应用中,objectMapper.readValue()和objectMapper.writeValueAsString()方法可能会抛出JsonProcessingException(或其子类IOException),因此需要进行适当的异常捕获和处理。
-
依赖管理: 确保项目中已正确引入Jackson库的依赖,例如jackson-databind和jackson-annotations。
com.fasterxml.jackson.core jackson-databind 2.13.4 com.fasterxml.jackson.core jackson-annotations 2.13.4 org.projectlombok lombok 1.18.24 provided
通过掌握这些技巧,开发者可以灵活应对各种复杂的JSON数据结构,高效地进行序列化和反序列化操作。










