在 Java 裡面用 ObjectMapper(Jackson)處理 JSON 再正常不過了,但一旦涉及到泛型,事情就開始變得有點複雜。我之前在這邊踩過好幾次坑,最後發現居然是反序列化出來的不是我預期的物件。

為什麼泛型會出問題?

先說為什麼會有這個問題。Java 的泛型是編譯時的特性,在 runtime 時會被擦除。也就是說,List<MyObj>List<String> 在 runtime 時看起來都是一樣的 List,沒辦法區分。

所以當你這樣寫的時候:

1
2
String json = "[{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]";
List<MyObj> list = objectMapper.readValue(json, List.class);

Jackson 不知道你想要 List<MyObj>,反序列化出來的就是 List<LinkedHashMap>——每個元素都是 LinkedHashMap,不是 MyObj。

1
2
3
for (Object obj : list) {
System.out.println(obj.getClass()); // class java.util.LinkedHashMap
}

看吧,就爆了。

解決方案一:TypeReference

最常見的解決方式就是用 TypeReference。這是一個神奇的類別,可以保留泛型資訊到 runtime。

1
2
String json = "[{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]";
List<MyObj> list = objectMapper.readValue(json, new TypeReference<List<MyObj>>(){});

看起來有點奇怪,那個空的 {} 是什麼?那叫做匿名內部類別。它的作用就是在 runtime 時保留 List<MyObj> 這個泛型資訊,讓 Jackson 知道怎麼反序列化。

你也可以用這種方式處理更複雜的泛型:

1
2
3
4
5
6
7
// Map<String, List<MyObj>>
Map<String, List<MyObj>> map = objectMapper.readValue(json,
new TypeReference<Map<String, List<MyObj>>>(){});

// List<Map<String, Object>>
List<Map<String, Object>> complexList = objectMapper.readValue(json,
new TypeReference<List<Map<String, Object>>>(){});

解決方案二:JavaType

如果你想要避免每次都寫 TypeReference,或者泛型資訊是動態的,可以用 JavaType

1
2
3
JavaType javaType = objectMapper.getTypeFactory()
.constructParametricType(List.class, MyObj.class);
List<MyObj> list = objectMapper.readValue(json, javaType);

或者更直接的方式(如果只是 List):

1
2
3
JavaType javaType = objectMapper.getTypeFactory()
.constructCollectionType(List.class, MyObj.class);
List<MyObj> list = objectMapper.readValue(json, javaType);

如果是 Map:

1
2
3
JavaType javaType = objectMapper.getTypeFactory()
.constructMapType(Map.class, String.class, MyObj.class);
Map<String, MyObj> map = objectMapper.readValue(json, javaType);

實務例子

來個完整的例子,假設你有這些 class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User {
private String name;
private int age;

// getter/setter...
}

public class Response<T> {
private int code;
private String message;
private T data;

// getter/setter...
}

現在你要反序列化一個 Response<List<User>>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String json = """
{
"code": 200,
"message": "success",
"data": [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30}
]
}
""";

// 用 TypeReference
Response<List<User>> response = objectMapper.readValue(json,
new TypeReference<Response<List<User>>>(){});

System.out.println(response.getCode()); // 200
System.out.println(response.getData().get(0).getName()); // Alice

或者用 JavaType:

1
2
3
4
5
JavaType userListType = objectMapper.getTypeFactory()
.constructCollectionType(List.class, User.class);
JavaType responseType = objectMapper.getTypeFactory()
.constructParametricType(Response.class, userListType);
Response<List<User>> response = objectMapper.readValue(json, responseType);

序列化呢?

序列化就簡單很多,因為你已經有實際的物件了,泛型資訊不會丟失:

1
2
3
4
5
6
7
8
List<User> users = Arrays.asList(
new User("Alice", 25),
new User("Bob", 30)
);

String json = objectMapper.writeValueAsString(users);
System.out.println(json);
// [{"name":"Alice","age":25},{"name":"Bob","age":30}]

常見的坑

1. 忘記用 TypeReference,導致反序列化錯誤

1
2
3
4
5
6
// 錯誤做法
List<User> users = objectMapper.readValue(json, List.class);
// 實際上 users 裡面是 LinkedHashMap,不是 User

// 正確做法
List<User> users = objectMapper.readValue(json, new TypeReference<List<User>>(){});

2. 嵌套泛型時寫反了

1
2
3
4
5
6
7
8
// 如果你想要 Map<String, List<User>>
// 這樣是對的
Map<String, List<User>> map = objectMapper.readValue(json,
new TypeReference<Map<String, List<User>>>(){});

// 千萬不要這樣寫
Map<String, List<User>> map = objectMapper.readValue(json,
new TypeReference<List<Map<String, User>>>(){});

3. 泛型邊界沒有正確處理

如果你的 class 有泛型邊界,比如:

1
2
3
public class Container<T extends User> {
private T data;
}

反序列化的時候要指定具體的型別:

1
2
3
4
5
6
7
// 這樣是對的
Container<User> container = objectMapper.readValue(json,
new TypeReference<Container<User>>(){});

// 或者
Container<AdminUser> container = objectMapper.readValue(json,
new TypeReference<Container<AdminUser>>(){});

ObjectMapper 的其他設定

順便講一下 ObjectMapper 的一些常用設定,有時候會影響反序列化的行為:

1
2
3
4
5
6
7
8
9
10
11
12
13
ObjectMapper mapper = new ObjectMapper();

// 忽略未知的屬性
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// 允許空的 JSON 字串
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);

// 使用 BigDecimal 代替 Double(避免精度問題)
mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);

// 允許單引號
mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);

重點整理

  • Java 泛型在 runtime 會被擦除,所以直接用 List.class 是不夠的
  • TypeReference<T>(){} 來保留泛型資訊,這是最常見的方式
  • 或者用 JavaType + getTypeFactory() 來構造複雜的泛型型別
  • 序列化不用擔心,Jackson 會自動處理
  • 常見的坑就是忘記用 TypeReference,導致反序列化出 LinkedHashMap

下次再碰到 JSON 泛型問題的時候,記得用 TypeReference,就不會踩坑了。