為什麼 JSON 序列化一致性很重要
1. 前端解析與型態安全
當 API 回傳的數字型態不一致時,JavaScript 前端可能遇到嚴重問題。例如:
- TypeScript 型態檢查失敗:定義為
number的欄位收到string - 數值運算錯誤:
"100" + 50結果是"10050"而非150 - 條件判斷異常:
"0" == false為 true,但"0" === false為 false
2. 跨環境一致性
開發、測試、生產環境的 API 回應應保持完全一致。不同環境序列化設定差異可能導致:
- 測試環境通過,生產環境失敗
- 前端 Mock 資料與實際 API 不符
- 第三方系統整合時的資料型態衝突
3. API 穩定性與向後相容
一旦 API 公開,變更回應格式會破壞現有客戶端。統一序列化設定可確保:
- 版本升級不會意外改變 JSON 格式
- 多個服務之間的資料交換保持一致
- 減少因型態不一致導致的客戶端錯誤
問題背景
在使用 Spring Boot 開發應用程式時,我們經常需要將物件序列化為 JSON 格式以進行數據傳輸或存儲。然而,不同環境中 JSON 回傳的資料型態可能會出現不一致的情況。
典型問題場景:
- A 環境:Java API 回傳的
Long型別數字被序列化為"12345"(字串) - B 環境:相同欄位被序列化為
12345(數字)
這種不一致可能導致前端解析錯誤、TypeScript 型態檢查失敗,或其他系統集成問題。了解並解決這一問題,對於保持系統的穩定性和一致性至關重要。
Jackson 序列化配置
Jackson 是 Spring Boot 預設的 JSON 序列化/反序列化庫。其行為可以通過配置文件或程式碼進行調整。以下是影響數字型資料表現形式的關鍵配置:
關鍵配置說明
| 配置項目 | 預設值 | 作用 | 影響 |
|---|---|---|---|
WRITE_DATES_AS_TIMESTAMPS |
true |
日期序列化為時間戳 | 影響 Date、LocalDateTime 等型態 |
WRITE_NUMBERS_AS_STRINGS |
false |
數字序列化為字串 | 影響 Long、Integer、Double 等型態 |
重要提醒:Jackson 預設行為是將數字序列化為其原始型態(例如 Long 序列化為 JSON 數字),而非字串。如果您的 A 環境出現字串型態,代表該環境啟用了 WRITE_NUMBERS_AS_STRINGS=true。
配置方法
方法一:application.properties 配置
# 日期序列化設定(false = ISO-8601 字串格式,true = Unix 時間戳)
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
# 數字序列化設定(false = 數字型態,true = 字串型態)
spring.jackson.serialization.WRITE_NUMBERS_AS_STRINGS=false
方法二:application.yml 配置
spring:
jackson:
serialization:
WRITE_DATES_AS_TIMESTAMPS: false # 日期使用 ISO-8601 格式
WRITE_NUMBERS_AS_STRINGS: false # 數字保持原始型態
方法三:自定義 ObjectMapper(程式碼配置)
另一種方法是直接在 Java 程式碼中自定義 ObjectMapper。這可以確保在所有環境中使用相同的 Jackson 配置:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 日期序列化為 ISO-8601 格式,而非 Unix 時間戳
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
// 數字保持原始型態(Long, Integer 等),而非字串
mapper.configure(SerializationFeature.WRITE_NUMBERS_AS_STRINGS, false);
return mapper;
}
}
預設行為與覆寫
Jackson 預設序列化行為
- 數字型態:保持原始型態(
Long→ JSON 數字) - 日期型態:序列化為 Unix 時間戳(
1672531200000)
如何產生字串型態序列化
若要將數字序列化為字串(通常不建議),需明確啟用:
spring.jackson.serialization.WRITE_NUMBERS_AS_STRINGS=true
序列化結果範例:
{
"userId": "12345", // 啟用 WRITE_NUMBERS_AS_STRINGS
"balance": "1000.50", // 所有數字都變成字串
"timestamp": 1672531200000 // 日期仍為時間戳(除非另外設定)
}
最佳實踐建議
- 統一所有環境的配置
- 在
application.properties或application.yml中明確設定 - 避免依賴預設值,確保配置可見性
- 在
- 版本控制配置檔
- 將 Jackson 配置納入 Git 版本控制
- 確保開發、測試、生產環境使用相同配置
- 優先使用數字型態
- 保持
WRITE_NUMBERS_AS_STRINGS=false(預設值) - 避免不必要的型態轉換開銷
- 保持
- 日期使用 ISO-8601 格式
- 設定
WRITE_DATES_AS_TIMESTAMPS=false - 提高可讀性與跨系統相容性
- 設定
- 整合測試驗證
- 撰寫測試案例驗證 JSON 序列化結果
- 確保型態符合 API 規格
常見問題 FAQ
Q1: 為什麼 A 環境序列化為 String,B 環境為 Long?
A: 最常見原因是兩個環境的 application.properties 設定不同。檢查以下項目:
- A 環境可能設定了
WRITE_NUMBERS_AS_STRINGS=true - B 環境使用預設值(
false) - 或兩個環境使用了不同版本的 Jackson 函式庫
解決方法:統一配置檔,並確保所有環境使用相同版本的依賴。
Q2: WRITE_NUMBERS_AS_STRINGS 的預設值是什麼?
A: 預設值為 false,也就是說 Jackson 預設會將數字保持為 JSON 數字型態,而非字串。如果您的環境出現字串序列化,代表該設定被明確改為 true。
Q3: 如何確認目前環境的序列化設定?
A: 可透過以下方式檢查:
@Autowired
private ObjectMapper objectMapper;
public void checkConfig() {
boolean numbersAsStrings = objectMapper.isEnabled(SerializationFeature.WRITE_NUMBERS_AS_STRINGS);
boolean datesAsTimestamps = objectMapper.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
System.out.println("WRITE_NUMBERS_AS_STRINGS: " + numbersAsStrings);
System.out.println("WRITE_DATES_AS_TIMESTAMPS: " + datesAsTimestamps);
}
Q4: 可以針對特定欄位設定序列化方式嗎?
A: 可以使用 @JsonSerialize 或 @JsonFormat 註解針對特定欄位自訂序列化行為:
public class UserResponse {
@JsonSerialize(using = ToStringSerializer.class)
private Long userId; // 強制序列化為字串
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
private Long balance; // 強制序列化為數字
}
Q5: 生產環境已運行,如何安全變更設定?
A: 建議採用漸進式部署策略:
- API 版本控制:建立新版本 API(如
/v2/users),使用新的序列化設定 - 向後相容:保留舊版本 API,給客戶端時間遷移
- 逐步切換:監控錯誤率,確認無影響後再完全切換
- 文件更新:明確告知 API 使用者型態變更
Q6: 如何測試序列化結果?
A: 可撰寫整合測試驗證 JSON 格式:
@SpringBootTest
@AutoConfigureMockMvc
public class SerializationTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testUserResponseSerialization() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.userId").isNumber()) // 確認為數字型態
.andExpect(jsonPath("$.balance").isNumber());
}
}
疑難排解
問題:設定了 WRITE_NUMBERS_AS_STRINGS=false,但仍序列化為字串
可能原因:
- 欄位使用了
@JsonSerialize(using = ToStringSerializer.class)註解 - 自訂的
ObjectMapperBean 覆蓋了配置檔設定 - 使用了第三方函式庫的序列化器
解決方法:檢查 Java 類別的註解,並確認沒有其他 Bean 覆寫 ObjectMapper 配置。
問題:前端收到的數字超過 JavaScript 安全整數範圍
背景:JavaScript 的 Number.MAX_SAFE_INTEGER 為 2^53 - 1(約 9 千兆),超過此範圍會失去精度。
解決方法:對於大數字(如 Twitter ID、資料庫 BIGINT),應序列化為字串:
public class TweetResponse {
@JsonSerialize(using = ToStringSerializer.class)
private Long tweetId; // 超過 JavaScript 安全整數範圍,使用字串
}
總結
為了在不同環境中保持 JSON 回傳資料型態的一致性,建議在 Spring Boot 應用中統一 Jackson 的序列化配置。通過修改配置文件或自定義 ObjectMapper,可以有效解決數字型資料在不同環境中被序列化為不同型態的問題。這樣可以確保系統在不同環境中的穩定性和一致性。
核心建議:
- 明確設定
WRITE_NUMBERS_AS_STRINGS=false(保持數字型態) - 明確設定
WRITE_DATES_AS_TIMESTAMPS=false(使用 ISO-8601 格式) - 在所有環境使用相同的配置檔
- 撰寫整合測試驗證序列化結果
- 對於超過 JavaScript 安全整數範圍的數字,使用
@JsonSerialize註解強制序列化為字串
Related Articles
- Spring Boot JSON Serialization: Solving String vs Long Type Inconsistency
- How to Troubleshoot and Resolve Spring Boot Configuration Default Value Issues
- Quartz Clustering Complete Guide: Preventing Duplicate Task Execution in ECS Multi-Container Environments
- Multiple Methods for Image Upload in Java Web Applications
- AWS Outage Deep Dive: Multi-Cloud Disaster Recovery Strategies for Architects