問題背景
最近在開發過程中遇到一個詭異的問題:呼叫某個 API 後,某個常數 name 的值居然是 nil,但從 raw data 看起來明明有值。
症狀檢查清單:
- ✅ Console 印出 raw data 看起來正常
- ✅
jsonDecode解碼成功 - ✅ Enum 對應的 JSON key (
_Name_Ch) 完全相同 - ✅ 瀏覽器中直接訪問 API,
name確實有值 - ❌ Swift 中取得的
name卻是nil
經過反覆檢查,終於發現問題根源:不可見的 BOM (Byte-Order Mark) 字元。
什麼是 BOM?
BOM (Byte-Order Mark),中文稱為位元組順序記號,是一個不可見的 Unicode 字元,用於標示文字檔的編碼位元組順序。
常見的 BOM 字元:
- UTF-8 BOM:
0xEF 0xBB 0xBF(Unicode:U+FEFF) - UTF-16 BE BOM:
0xFE 0xFF - UTF-16 LE BOM:
0xFF 0xFE
問題診斷
根據問題分析,name 為 nil 的原因是:
API 回應的 JSON 資料中,
_Name_Ch這個 key 的前面有一個不可見的 BOM 字元,導致 Swift 的 JSON 解碼器無法正確匹配 key 名稱。
實際的 JSON key 對比:
// 我們期望的 key
"_Name_Ch"
// 實際的 key(包含不可見的 BOM)
"\u{feff}_Name_Ch" // \u{feff} 就是 UTF-8 BOM
由於 key 名稱不匹配,JSONDecoder 找不到對應的欄位,因此 name 變成 nil。
解決方案
方法 1:清除 BOM 字元(推薦)
在解析 JSON 之前,先移除所有 BOM 字元:
// Swift 解決方案
func cleanBOM(from jsonString: String) -> String {
return jsonString.replacingOccurrences(of: "\u{feff}", with: "")
}
// 使用方式
let rawJSON = String(data: responseData, encoding: .utf8) ?? ""
let cleanedJSON = cleanBOM(from: rawJSON)
// 然後再進行 JSON 解碼
if let data = cleanedJSON.data(using: .utf8) {
let decoded = try JSONDecoder().decode(YourModel.self, from: data)
}
方法 2:建立 String Extension(可重用)
如果需要在多處使用,建議建立 extension:
extension String {
/// 移除字串中的 BOM (Byte-Order Mark) 字元
func removingBOM() -> String {
var result = self
// 移除 UTF-8 BOM
result = result.replacingOccurrences(of: "\u{feff}", with: "")
// 移除 UTF-16 BE BOM(較少見)
result = result.replacingOccurrences(of: "\u{fffe}", with: "")
return result
}
}
// 使用方式
let cleanedJSON = rawJSON.removingBOM()
方法 3:後端修正(根本解決)
最佳做法是請後端團隊修正 API,確保回應的 JSON 不包含 BOM:
// Node.js Express 範例
app.get('/api/data', (req, res) => {
const data = fetchData()
// 確保 Content-Type 正確,明確指定編碼
res.setHeader('Content-Type', 'application/json; charset=utf-8')
// 使用 JSON.stringify 避免 BOM 問題
res.send(JSON.stringify(data))
})
# Python Flask 範例
@app.route('/api/data')
def get_data():
data = fetch_data()
# Flask 的 jsonify 會自動設定正確的編碼
return jsonify(data), 200, {'Content-Type': 'application/json; charset=utf-8'}
為什麼只有特定欄位受影響?
BOM 通常出現在以下情況:
檔案開頭
- Windows 記事本等編輯器會自動在檔案開頭加入 BOM
- 如果 JSON 資料來自檔案讀取,可能帶有 BOM
字串拼接
- 從不同來源拼接的資料可能意外帶入 BOM
- 例如:從資料庫讀取 + 手動拼接字串
資料庫匯出
- 某些資料庫工具(如 Excel 匯出 CSV)會加入 BOM
- 如果後端從這些來源讀取資料,可能保留 BOM
檔案編碼轉換
- 編碼轉換過程中意外引入 BOM
- 例如:UTF-16 轉 UTF-8 時沒有正確處理
如果只有 name 欄位受影響,可能是因為:
- 該欄位的值來自不同的資料來源
- 該欄位經過特殊的處理或轉換
- 資料輸入時使用了包含 BOM 的編輯器
預防措施
開發階段
1. 檢查 API 回應是否有 BOM
使用 hexdump 或類似工具檢查原始位元組:
# 使用 hexdump 檢查檔案開頭
hexdump -C response.json | head
# 如果看到 "ef bb bf" 開頭,就是 UTF-8 BOM
00000000 ef bb bf 7b 22 6e 61 6d 65 22 3a 22 76 61 6c 75 |...{"name":"valu|
2. 配置編輯器避免 BOM
// VS Code 設定 (.vscode/settings.json)
{
"files.encoding": "utf8", // 使用 UTF-8 without BOM
"files.autoGuessEncoding": false
}
// Sublime Text 設定
Preferences → Settings
{
"default_encoding": "UTF-8",
"fallback_encoding": "UTF-8"
}
測試階段
建立自動化測試檢查 BOM:
import XCTest
class APITests: XCTestCase {
func testNoBOMInAPIResponse() async throws {
// 呼叫 API
let response = try await APIClient.shared.fetchData()
// 轉換為字串檢查
let jsonString = String(data: response, encoding: .utf8)
// 斷言不包含 BOM
XCTAssertFalse(
jsonString?.hasPrefix("\u{feff}") ?? false,
"API response should not contain BOM character"
)
}
func testJSONDecodingWithBOM() throws {
// 測試解碼器能否處理帶 BOM 的 JSON
let bomJSON = "\u{feff}{\"name\":\"Test\"}"
let data = bomJSON.data(using: .utf8)!
// 這個測試應該會失敗(除非有處理 BOM)
XCTAssertThrowsError(
try JSONDecoder().decode(TestModel.self, from: data)
)
// 清除 BOM 後應該成功
let cleanedData = bomJSON.removingBOM().data(using: .utf8)!
XCTAssertNoThrow(
try JSONDecoder().decode(TestModel.self, from: cleanedData)
)
}
}
生產環境監控
在 API Client 中加入 BOM 偵測和警告:
class APIClient {
func fetchData() async throws -> Data {
let (data, response) = try await URLSession.shared.data(from: url)
// 檢查是否有 BOM
if let jsonString = String(data: data, encoding: .utf8),
jsonString.hasPrefix("\u{feff}") {
// 記錄警告到分析工具
Analytics.logWarning("API response contains BOM", metadata: [
"endpoint": url.absoluteString
])
// 自動清理 BOM
let cleaned = jsonString.removingBOM()
return cleaned.data(using: .utf8) ?? data
}
return data
}
}
結論
不可見的 BOM 字元是 JSON 解析中一個隱藏但常見的陷阱。當遇到看似正常的 JSON 卻無法正確解析時,記得檢查是否有 BOM 字元的存在。
關鍵要點:
- ✅ BOM 是不可見的 Unicode 字元,用於標示編碼順序
- ✅ 會導致 JSON key 名稱匹配失敗
- ✅ 可以用字串替換方法移除
- ✅ 最好從 API 後端根除問題
- ✅ 建立自動化測試和監控機制
最佳實踐:
- 開發環境:確保編輯器不會自動加入 BOM
- API 開發:後端確保輸出不包含 BOM
- 客戶端:建立防禦性程式碼,自動清理 BOM
- 測試:加入 BOM 偵測的自動化測試