The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

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

今天公司同事在用 .NET 處理一個系統串接需求時,發現對方傳來的 JSON 格式會把應該為「數值」的數字資料使用「字串」的格式來表達,這導致他在使用 System.Text.JsonJsonSerializer.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 的正確使用方式!

相關連結