PHP讀JSON得到null:分清合法null與解碼錯誤

PHP讀取JSON後得到null,不一定表示解碼失敗。JSON本身可以是一個合法的null;但格式錯誤時,預設的json_decode()也可能回傳null。若程式只寫「結果為null就報錯」,便會把有效資料當成錯誤。需要可靠區分時,可以用JSON_THROW_ON_ERROR,把解碼失敗交給JsonException處理。

這個問題容易出現在匯入檔案、第三方API回應及儲存JSON設定的功能裡。畫面只顯示「資料是空的」,站長卻不知道是服務正常回傳空值,還是資料傳到一半壞掉。先把語法解析與資料格式檢查拆開,錯誤訊息才會說到真正的問題。

合法null和壞掉的字串長得不一樣

JSON的null不需要引號,也沒有大括號。字串'null'交給json_decode後會成為PHP的null。相反,'{"name":}'缺少值,屬於無效JSON。預設解碼這兩者,都可能得到相同的null結果,只看回傳值無法分辨。

空字串也不是JSON的null。API回應沒有內容、HTTP請求失敗、檔案讀取失敗和伺服器回傳HTML錯誤頁,各自有不同原因;不能把它們先改成'null'再繼續處理。應先確認讀取或請求成功,再把真正拿到的字串交給解析器。

JSON也不只允許物件或陣列。像false、0和一段有雙引號包住的字串,都可以是合法值。因此「解碼沒有拋例外」只證明語法能解析,還不能證明資料符合某個API要求的物件結構。

用四組輸入看清差別

以下程式只使用合成字串,可儲存為case.php後在命令列執行。JSON_THROW_ON_ERROR自PHP7.3提供;本文的輸出來自PHP8.4.2。先用普通模式記錄錯誤,再用例外模式解析同一份資料。

<?php
$cases = [
    'valid-null' => 'null',
    'valid-object' => '{"name":"demo"}',
    'broken' => '{"name":}',
    'empty' => '',
];

foreach ($cases as $label => $raw) {
    $plain = json_decode($raw, true);
    $error = json_last_error();
    printf("%s plain=%s error=%d\n", $label,
        var_export($plain, true), $error);
    try {
        $value = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
        echo "throw-mode: ", var_export($value, true), PHP_EOL;
    } catch (JsonException $e) {
        echo "throw-mode: JsonException code=", $e->getCode(), PHP_EOL;
    }
}

合法null在普通模式的錯誤碼為0,例外模式仍然回傳null,沒有進入catch。合法物件會得到包含name的陣列。壞掉的字串與空字串在這組測試裡都得到語法錯誤碼4,例外模式則拋出JsonException。寫應用程式時應用JSON_ERROR_NONE等常數判斷,不要把這些數字硬寫成你的業務規則。

這樣你就能把「資料確實是null」與「資料無法解碼」放到不同分支。若對方的API規格允許null代表沒有設定,便可以在成功解析後處理這種狀態;若規格要求物件,則回報資料結構不符,而不是誤報語法壞掉。

把解析放在一個小的try區塊

try {
    $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    throw new RuntimeException('回應不是有效JSON', 0, $e);
}

if (!is_array($data) || !array_key_exists('name', $data)
    || !is_string($data['name'])) {
    throw new UnexpectedValueException('回應缺少有效name');
}

例子先處理解碼失敗,再確認結果能不能依name欄位使用。資料null、false或缺少name時,不會被當成合法會員資料往下傳。true參數會把JSON物件解成PHP陣列;JSON陣列也會是PHP陣列,所以更嚴格的API規格仍要另外確認資料形狀,不能靠is_array分清所有情況。

try裡只放解析,讀起來也比較清楚:JsonException來自JSON解析,而後面的欄位規則有自己的錯誤。不要用一個很大的catch把網路連線、解析、資料庫寫入的錯誤都改成「JSON錯誤」。保留原例外作為前一個例外,也讓維護者有機會追查原因。

舊程式不丟例外時,要立即讀錯誤狀態

$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException('JSON解析失敗:' . json_last_error_msg());
}

如果你暫時沿用普通模式,應在這次解析後立即看錯誤狀態,避免中間另一個JSON操作把它改掉。不要先做其他編碼、再回頭判斷前一份資料。錯誤訊息可以協助維護者排查,但公開回應仍應使用讀者能理解的提示;也不要把含個人資料、憑證或完整回應內容直接寫入公開頁面。

選用JSON_THROW_ON_ERROR後,該次操作以例外回報JSON錯誤,不應再把json_last_error當成它的判斷依據。全域錯誤狀態可能仍是先前一次普通模式的結果,所以「沒有拋例外,但last_error不是0」不一定矛盾。選定一種解析方式,在同一個分支內一致處理即可。

深度與編碼也會讓解析失敗

無效JSON不只包含漏逗號或缺引號。字串必須使用有效UTF-8,資料巢狀深度也受depth參數限制。本文範例使用512,並不表示所有輸入都應無條件放寬。若上游送來不正常的深層資料,應先核對契約與內容,再決定是否調整限制。

另外,depth參數本身超出允許範圍,在PHP8會拋出ValueError;它不是JsonException,不會被上面的catch捕捉。這通常是程式參數問題,應修正設定。不要為了讓匯入繼續而把所有Throwable都吞掉,否則真正的程式錯誤會被藏成一個空結果。

回應錯誤要讓使用者知道下一步

匯入設定時,使用者需要的提示是「檔案格式無法讀取」或「缺少必要欄位」,而不是一個沒有上下文的null。前一種提示可請他確認檔案格式、重新匯出;後一種則能指出哪個欄位不符合要求。兩者的修復方式不同,應保留解析失敗與資料驗證失敗的分別。

如果資料來自你管理的另一個服務,可以在內部紀錄請求識別碼、狀態碼及錯誤類別,讓維護者找回相應的回應;若來自外部服務,則按它的文件核對內容格式。記錄時保留排查需要的資料就好,不要為了方便把整份用戶設定、授權標頭或上游回應公開顯示。

例如第三方服務正常回傳null,你的功能卻要求物件,這是雙方資料約定需要處理,不是JSON語法錯誤。若服務回傳HTML錯誤頁,才是解析器收到了不應交給它的內容。把來源與解析兩段連起來看,會比單純增加catch更快找到問題。

大整數不要在解析時悄悄失去精度

如果JSON包含長度很大的數字識別碼,還要留意PHP解碼後的型別。超過整數可表示範圍的值可能變成浮點數,後續比較或重新輸出時就不適合當精確識別碼。這類資料應依API契約處理,可以使用JSON_BIGINT_AS_STRING保留大整數的文字表示,再與JSON_THROW_ON_ERROR合併使用。

這個旗標不是把所有數字都變成字串,也不會修正其他欄位的型別。若原本就能協調API格式,以字串傳遞識別碼通常更容易維持一致;接收端也應明確驗證字串內容。不要看到解析成功就立刻把識別碼轉成int,否則可能在下一步丟掉你剛保住的資料。

驗證功能時,別只留一份成功資料

保留合法物件、合法null、空字串、破損JSON四份測試,再依功能加入false、陣列或缺欄位的輸入。解析層應正確區分有效JSON與語法錯誤,資料層則確認各種有效值是否符合用途。兩層分開後,你才能回答「這是正常空值,還是回應壞掉」。

若問題來自API,最後還要確認狀態碼、回應內容及上游服務的約定;換上例外旗標不會修好網路連線,也不會保證資料正確。對網站維護而言,清楚報告失敗的位置,比把任何null都換成空陣列更容易找出下一步該處理什麼。

參考資料

原文鏈接:https://wntheme.com/php-json-decode-null-exception/,轉載請註明出處。
0

評論0

顯示驗證碼
沒有帳號?註冊  忘記密碼?