解決 Spring Boot JSON 序列化問題:string vs long

🌏 Read the English version

為什麼 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 日期序列化為時間戳 影響 DateLocalDateTime 等型態
WRITE_NUMBERS_AS_STRINGS false 數字序列化為字串 影響 LongIntegerDouble 等型態

重要提醒: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  // 日期仍為時間戳(除非另外設定)
}

最佳實踐建議

  1. 統一所有環境的配置
    • application.propertiesapplication.yml 中明確設定
    • 避免依賴預設值,確保配置可見性
  2. 版本控制配置檔
    • 將 Jackson 配置納入 Git 版本控制
    • 確保開發、測試、生產環境使用相同配置
  3. 優先使用數字型態
    • 保持 WRITE_NUMBERS_AS_STRINGS=false(預設值)
    • 避免不必要的型態轉換開銷
  4. 日期使用 ISO-8601 格式
    • 設定 WRITE_DATES_AS_TIMESTAMPS=false
    • 提高可讀性與跨系統相容性
  5. 整合測試驗證
    • 撰寫測試案例驗證 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: 建議採用漸進式部署策略:

  1. API 版本控制:建立新版本 API(如 /v2/users),使用新的序列化設定
  2. 向後相容:保留舊版本 API,給客戶端時間遷移
  3. 逐步切換:監控錯誤率,確認無影響後再完全切換
  4. 文件更新:明確告知 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) 註解
  • 自訂的 ObjectMapper Bean 覆蓋了配置檔設定
  • 使用了第三方函式庫的序列化器

解決方法:檢查 Java 類別的註解,並確認沒有其他 Bean 覆寫 ObjectMapper 配置。

問題:前端收到的數字超過 JavaScript 安全整數範圍

背景:JavaScript 的 Number.MAX_SAFE_INTEGER2^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

Leave a Comment