常見情境:呼叫外部 HTTP API 取得 JSON 字串,json_decode($str, true) 永遠回傳 null,但將回應內容複製到程式碼中硬編碼卻能正常解析。主因通常是 **UTF-8 BOM (EF BB BF)** 隱藏在字串開頭,導致 JSON 解析器誤判格式。本文提供完整偵測與移除方案,並延伸至其他不可見字元處理。
$raw = file_get_contents('https://api.example.com/data.json');
var_dump($raw); // string(103) "{"id":1,"name":"測試"}" ← 外觀正常
$data = json_decode($raw, true);
var_dump($data); // NULL
var_dump(json_last_error()); // 4 (JSON_ERROR_SYNTAX)
var_dump(json_last_error_msg()); // "Syntax error"
function hasBom(string $str): bool
{
return strlen($str) >= 3 && $str[0] === "\xEF" && $str[1] === "\xBB" && $str[2] === "\xBF";
}
// 或使用 pack (可讀性較佳)
function hasBomPack(string $str): bool
{
return substr($str, 0, 3) === pack('CCC', 0xEF, 0xBB, 0xBF);
}
// 十六進位檢視確認
echo bin2hex(substr($raw, 0, 10)); // efbbbf7b226964223a31 -> 前三位 EF BB BF 即 BOM
/**
* 移除字串開頭的 UTF-8 BOM (若存在)
* @param string $str
* @return string 無 BOM 字串
*/
function stripBom(string $str): string
{
$bom = pack('CCC', 0xEF, 0xBB, 0xBF);
if (str_starts_with($str, $bom)) { // PHP 8.0+
return substr($str, 3);
}
// PHP 7 相容
return substr($str, 0, 3) === $bom ? substr($str, 3) : $str;
}
// 使用
$clean = stripBom($raw);
$data = json_decode($clean, true); // 正常解析
function sanitizeJsonString(string $str): string
{
// 1. 移除 BOM
$str = stripBom($str);
// 2. 移除零寬空格 (U+200B, U+200C, U+200D, U+FEFF 等)
$str = preg_replace('/[\x{200B}-\x{200D}\x{FEFF}]/u', '', $str);
// 3. 移除開頭/結尾空白字元 (含不可見)
$str = trim($str, "\x00..\x1F\x7F\xA0\x{2000}-\x{200F}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}\x{FEFF}");
return $str;
}
function safeJsonDecode(string $raw, bool $assoc = true, int $depth = 512, int $flags = 0): mixed
{
$clean = sanitizeJsonString($raw);
$result = json_decode($clean, $assoc, $depth, $flags);
if (json_last_error() !== JSON_ERROR_NONE) {
// 記錄前 200 字元供除錯
$preview = mb_substr($clean, 0, 200);
throw new RuntimeException(
"JSON 解析失敗:{$preview}... | " . json_last_error_msg()
);
}
return $result;
}
// 使用
try {
$data = safeJsonDecode(file_get_contents('https://api.example.com/data.json'));
} catch (Throwable $e) {
error_log($e->getMessage());
// 降級處理...
}
| 來源 | 原因 | 預防 |
|---|---|---|
| Windows 記事本儲存 UTF-8 | 預設加 BOM | 使用 VS Code / Notepad++ 另存「UTF-8 without BOM」 |
| PHP include/require 載入設定檔 | 設定檔含 BOM | 所有 .php/.json 檔統一無 BOM |
| 外部 API 回應 | 對方伺服器輸出 BOM | 客戶端統一 stripBom() |
| 資料庫欄位存 JSON | 寫入時帶入 BOM | 寫入前 stripBom() |
| 前端 fetch 回應 | 後端輸出 BOM | 後端 header('Content-Type: application/json; charset=utf-8') 且無 BOM |
# 終端機快速檢查檔案是否有 BOM head -c 3 file.json | xxd # 輸出 ef bb bf 即有 BOM # 批次移除專案所有檔案 BOM (Linux/macOS) find . -type f -name '*.php' -o -name '*.json' -o -name '*.js' | xargs -r sed -i '1s/^\xEF\xBB\xBF//'
var_dump 不顯、json_decode 直接失敗。stripBom(),避免業務碼散落判斷。