HashMap 轉 JSON 看似簡單,但真正用上去踩坑才多。有三種常見的方式,各有優缺點,選對一個才不會被 JsonProcessingException 折磨。

場景說明

先假設有這樣的 HashMap:

1
2
3
4
5
6
7
8
9
10
11
Map<String, Object> userMap = new HashMap<>();
userMap.put("id", 123);
userMap.put("name", "Alice");
userMap.put("email", "[email protected]");
userMap.put("age", 30);
userMap.put("tags", Arrays.asList("admin", "developer"));

Map<String, Object> addressMap = new HashMap<>();
addressMap.put("city", "Taipei");
addressMap.put("country", "Taiwan");
userMap.put("address", addressMap);

要轉成 JSON 字串,有三種主流方案。

方案 1:org.json.simple.JSONObject

最直觀的做法,直接 new JSONObject(map):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 需要加入依賴
// <dependency>
// <groupId>com.googlecode.json-simple</groupId>
// <artifactId>json-simple</artifactId>
// <version>1.1.1</version>
// </dependency>

import org.json.simple.JSONObject;

Map<String, Object> userMap = new HashMap<>();
userMap.put("id", 123);
userMap.put("name", "Alice");
userMap.put("tags", Arrays.asList("admin", "developer"));

// 最簡單的寫法
String json = new JSONObject(userMap).toJSONString();
System.out.println(json);
// {"tags":["admin","developer"],"name":"Alice","id":123}

特點

1
2
3
4
5
6
// 支援嵌套 Map
Map<String, Object> outer = new HashMap<>();
outer.put("user", userMap);
outer.put("timestamp", System.currentTimeMillis());

String json = new JSONObject(outer).toJSONString();

優點

  1. 超直觀 - new JSONObject(map) 一句搞定
  2. 輕量級 - 依賴小,適合簡單專案
  3. 不拋 checked exception - toJSONString() 不會拋 JsonProcessingException

缺點

  1. 功能簡陋 - 沒有格式化、自訂序列化等高級功能
  2. 效能一般 - 大資料量時比較慢
  3. 不夠靈活 - null 處理、日期格式化都得自己來

方案 2:Jackson ObjectMapper - 最常用

Jackson 是現在最流行的 JSON 庫,Spring Boot 預設就內建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 通常不用加依賴,Spring 已經包含了
// 但獨立使用要加:
// <dependency>
// <groupId>com.fasterxml.jackson.core</groupId>
// <artifactId>jackson-databind</artifactId>
// <version>2.15.2</version>
// </dependency>

import com.fasterxml.jackson.databind.ObjectMapper;

Map<String, Object> userMap = new HashMap<>();
userMap.put("id", 123);
userMap.put("name", "Alice");
userMap.put("tags", Arrays.asList("admin", "developer"));

ObjectMapper mapper = new ObjectMapper();
try {
String json = mapper.writeValueAsString(userMap);
System.out.println(json);
// {"id":123,"name":"Alice","tags":["admin","developer"]}
} catch (JsonProcessingException e) {
e.printStackTrace();
}

推薦做法:複用 ObjectMapper

ObjectMapper 是重的物件,建立成本大。要複用同一個實例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class JsonService {

// 注入單一實例,全程複用
@Autowired
private ObjectMapper objectMapper;

// 或者自己建一個 bean
// @Bean
// public ObjectMapper objectMapper() {
// return new ObjectMapper();
// }

public String toJson(Map<String, Object> map) throws JsonProcessingException {
return objectMapper.writeValueAsString(map);
}
}

格式化輸出

1
2
3
4
5
6
7
8
9
// 一行行的 JSON(Pretty Print)
String json = mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(userMap);
System.out.println(json);
// {
// "id" : 123,
// "name" : "Alice",
// "tags" : [ "admin", "developer" ]
// }

自訂序列化

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

// 忽略 null 值
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

// 忽略未知的欄位(反序列化時)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// 允許沒有 getter/setter 的欄位(直接存取 field)
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

String json = mapper.writeValueAsString(userMap);

優點

  1. 功能最強 - 支援複雜序列化、自訂轉換器、日期格式等
  2. 效能最好 - 經過高度最佳化
  3. 生態完整 - Spring 全面支援,搭配 @JsonProperty、@JsonIgnore 等 annotation
  4. 廣泛使用 - 業界標準,團隊成員都會用

缺點

  1. 拋 JsonProcessingException - 必須 catch,或聲明 throws
  2. 設定複雜 - 各種 mapper 設定容易搞混
  3. 依賴相對重 - 相比 json-simple

方案 3:Google Gson - 最靈活

Google 出品,在某些場景比 Jackson 更靈活:

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
// <dependency>
// <groupId>com.google.code.gson</groupId>
// <artifactId>gson</artifactId>
// <version>2.10</version>
// </dependency>

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonElement;

Map<String, Object> userMap = new HashMap<>();
userMap.put("id", 123);
userMap.put("name", "Alice");
userMap.put("tags", Arrays.asList("admin", "developer"));

Gson gson = new Gson();

// 方法 1:直接 toJson
String json = gson.toJson(userMap);
System.out.println(json);
// {"id":123,"name":"Alice","tags":["admin","developer"]}

// 方法 2:先轉 JsonElement,更靈活
JsonElement jsonElement = gson.toJsonTree(userMap);
JsonObject jsonObject = jsonElement.getAsJsonObject();
System.out.println(jsonObject.toString());

格式化輸出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 建立 pretty 的 Gson
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.create();

String json = gson.toJson(userMap);
System.out.println(json);
// {
// "id": 123,
// "name": "Alice",
// "tags": [
// "admin",
// "developer"
// ]
// }

自訂序列化器

1
2
3
4
5
6
7
8
9
Gson gson = new GsonBuilder()
.registerTypeAdapter(LocalDateTime.class, new JsonSerializer<LocalDateTime>() {
@Override
public JsonElement serialize(LocalDateTime src, Type typeOfSrc,
JsonSerializationContext context) {
return new JsonPrimitive(src.format(DateTimeFormatter.ISO_DATE_TIME));
}
})
.create();

優點

  1. 簡潔 - toJson 方法超直觀,不拋 checked exception
  2. 靈活 - 可以先 toJsonTree,再修改 JsonObject,最後轉字串
  3. 適合複雜轉換 - 對於 null、特殊型別的處理比 Jackson 直觀
  4. 無依賴設定 - 開箱即用

缺點

  1. 性能稍差 - 比 Jackson 慢一點(但對大多數場景沒影響)
  2. 業界採用度低 - 不如 Jackson 常見
  3. 靈活性高導致代碼複雜 - 小任務很簡單,大任務容易亂

三種方案對比

項目 json-simple Jackson Gson
使用難度 最簡單 中等 簡單
功能完整性 弱 強 中等
效能 一般 最好 中等
異常處理 無 JsonProcessingException 無
生態支援 弱 強(Spring 全支援) 中等
版本穩定度 很舊(2010 年後沒更新) 最新維護中 定期更新
適合場景 簡單專案 企業應用、Spring 複雜轉換、Google 生態

實務選擇

推薦優先級

  1. Jackson ObjectMapper(90% 場景)

    • Spring Boot 預設就有
    • 功能最強、效能最好
    • 業界標準
  2. Google Gson(複雜轉換)

    • 需要靈活的欄位轉換
    • 不想處理 checked exception
  3. json-simple(小型專案、沒其他依賴)

    • 專案很小,不想加 Spring 依賴
    • 變成過時庫,除非必要別用

實務範例

場景:API response 轉 JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
public class UserController {

@Autowired
private ObjectMapper objectMapper;

@GetMapping("/user/{id}")
public String getUser(@PathVariable Long id) throws JsonProcessingException {
// 從 DB 取出資料,轉成 Map(為了示範)
Map<String, Object> user = new HashMap<>();
user.put("id", id);
user.put("name", "Alice");
user.put("email", "[email protected]");

// 直接回傳 Map,Spring 會自動用 ObjectMapper 序列化
// 但如果你想手動轉 JSON:
return objectMapper.writeValueAsString(user);
}
}

場景:嵌套的複雜結構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Map<String, Object> response = new HashMap<>();
response.put("code", 200);
response.put("message", "success");

List<Map<String, Object>> users = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
Map<String, Object> user = new HashMap<>();
user.put("id", i);
user.put("name", "User" + i);
users.add(user);
}

response.put("data", users);

// Jackson
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(response);

常見踩坑

坑 1:Jackson 沒初始化 ObjectMapper

1
2
3
4
5
6
7
8
9
10
// 錯誤 - 每次都新建,浪費資源
for (Map map : maps) {
String json = new ObjectMapper().writeValueAsString(map);
}

// 正確 - 複用一個實例
ObjectMapper mapper = new ObjectMapper();
for (Map map : maps) {
String json = mapper.writeValueAsString(map);
}

坑 2:Date 序列化亂碼

1
2
3
4
5
6
7
8
9
10
// 錯誤 - Date 會轉成 timestamp,看不懂
Map<String, Object> data = new HashMap<>();
data.put("created", new Date());
String json = mapper.writeValueAsString(data);
// {"created":1699884000000}

// 正確 - 自訂日期格式
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
String json = mapper.writeValueAsString(data);
// {"created":"2023-11-13 14:00:00"}

坑 3:Integer vs Long 精度丟失

1
2
3
4
5
// 危險 - long 值在 JavaScript 中會丟失精度
Map<String, Object> data = new HashMap<>();
data.put("id", 9223372036854775807L); // Long.MAX_VALUE
String json = mapper.writeValueAsString(data);
// JavaScript 只能精確表示到 2^53-1,會四捨五入

最後的話

HashMap 轉 JSON 真的小事一樁,但選對方案事半功倍。90% 時候用 Jackson ObjectMapper 就夠了,記得複用 mapper 實例就行。如果對效能沒要求,json-simple 也可以,但真的變 deprecated 了,不建議新專案用。