System.Text.Json 可使用 JsonSerializerDefaults.Web 處理常見的 JSON 格式

今天公司同事在用 .NET 處理一個系統串接需求時,發現對方傳來的 JSON 格式會把應該為「數值」的數字資料使用「字串」的格式來表達,這導致他在使用 System.Text.Json 的 JsonSerializer.Deserialize 進行反序列化時出現錯誤。這篇文章我來分享一個鮮為人知的小秘訣,讓你輕鬆駕馭各種 Web 常見的 JSON 格式。

JSON

問題說明

首先,我先假設我們的 JSON 資料長這樣,屬性命名採用 camelCase 規則,但 age 屬性使用 string 型別:

{
    "name": "Will",
    "birthday": "2000-06-11",
    "age": "18",
    "createdOn": "2000-06-11T19:53:03"
}

然後我們定義一個資料模型類別來表示這個結構,其中 Age 使用 int 型別:

public class UserProfile
{
    [JsonPropertyName("Name")]
    public string Name { get; set; }

    [JsonPropertyName("birthday")]
    public DateTime Birthday { get; set; }

    [JsonPropertyName("age")]
    public int Age { get; set; }

    [JsonPropertyName("createdOn")]
    public DateTime CreatedOn { get; set; }
}

最後,我們用 JsonSerializer.Deserialize 方法來進行反序列化操作:

var user = JsonSerializer.Deserialize<UserProfile>(jsonText);

此時你會立刻收到這個例外狀況:

The JSON value could not be converted to System.Int32. Path: $.age | LineNumber: 3 | BytePositionInLine: 15.

The JSON value could not be converted to System.Int32. Path: $.age | LineNumber: 3 | BytePositionInLine: 15.

遇到這種問題,你從錯誤訊息可以很清楚的判斷出,他沒辦法將 "18" 字串型別轉成 System.Int32 型別!

解決方案

要解決這個問題,主要有兩種:

  1. 自訂轉換器 (How to write custom converters for JSON serialization (marshalling) in .NET)

    自訂轉換器要額外定義一個類別,雖然結構不複雜,但實作上有比較麻煩些。

  2. 使用 System.Text.Json 內建的 JsonSerializerDefaults.Web 序列化選項

    這個解決方案真的出乎意外的簡單,多加一個參數即可:

    var user = JsonSerializer.Deserialize<UserProfile>(jsonText,
                  new JsonSerializerOptions(JsonSerializerDefaults.Web));
    

    注意: JsonSerializerDefaults 列舉是從 .NET 5 才開始有的型別。

認識 JsonSerializerDefaults.Web 列舉

你要是直接翻出 JsonSerializerOptions 建構式的原始碼出來看,就會發現箇中奧妙之處:

/// <summary>
/// Constructs a new <see cref="JsonSerializerOptions"/> instance with a predefined set of options determined by the specified <see cref="JsonSerializerDefaults"/>.
/// </summary>
/// <param name="defaults"> The <see cref="JsonSerializerDefaults"/> to reason about.</param>
public JsonSerializerOptions(JsonSerializerDefaults defaults) : this()
{
    if (defaults == JsonSerializerDefaults.Web)
    {
        _propertyNameCaseInsensitive = true;
        _jsonPropertyNamingPolicy = JsonNamingPolicy.CamelCase;
        _numberHandling = JsonNumberHandling.AllowReadingFromString;
    }
    else if (defaults != JsonSerializerDefaults.General)
    {
        throw new ArgumentOutOfRangeException(nameof(defaults));
    }
}

原來這個 JsonSerializerDefaults 列舉(Enum)的 JsonSerializerDefaults.Web 定義了三個序列化的選項(特性),其中包括:

  1. 屬性名稱會被視為不區分大小寫 (反序列化時使用)
  2. 屬性命名會採用 camelCase 命名規則 (序列化時使用)
  3. 允許從「字串」讀入「數值」型別的屬性 (反序列化時使用)

💡 查看 JsonSerializerDefaults 原始碼

由於 JsonSerializerDefaults.Web 包含了 3 個特性,如果你傳入的 JSON 有一個條件不符合,就建議不要這樣用。例如你傳入的 JSON 屬性名稱的命名規則採用 PascalCase 並且不區分大小寫的話,那你應該這樣撰寫程式碼:

var serializerOptions = new JsonSerializerOptions()
{
    PropertyNameCaseInsensitive = true,
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};

var user = JsonSerializer.Deserialize<UserProfile>(jsonText, serializerOptions);

如果你傳入的 JSON 屬性名稱的命名規則採用 camelCase 並且希望區分大小寫的話,那你應該這樣撰寫程式碼:

var serializerOptions = new JsonSerializerOptions()
{
    PropertyNameCaseInsensitive = false,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};

var user = JsonSerializer.Deserialize<UserProfile>(jsonText, serializerOptions);

以上才是 JsonSerializer.Deserialize 的正確使用方式!

相關連結

留言評論