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", "alice@example.com");
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", "alice@example.com");

// 直接回傳 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 了,不建議新專案用。