在 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()); }
|
看吧,就爆了。
解決方案一: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 = objectMapper.readValue(json, new TypeReference<Map<String, List<MyObj>>>(){});
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; }
public class Response<T> { private int code; private String message; private T data; }
|
現在你要反序列化一個 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} ] } """;
Response<List<User>> response = objectMapper.readValue(json, new TypeReference<Response<List<User>>>(){});
System.out.println(response.getCode()); System.out.println(response.getData().get(0).getName());
|
或者用 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);
|
常見的坑
1. 忘記用 TypeReference,導致反序列化錯誤
1 2 3 4 5 6
| List<User> users = objectMapper.readValue(json, List.class);
List<User> users = objectMapper.readValue(json, new TypeReference<List<User>>(){});
|
2. 嵌套泛型時寫反了
1 2 3 4 5 6 7 8
|
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);
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
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,就不會踩坑了。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️