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

今天公司同事在用 .NET 處理一個系統串接需求時，發現對方傳來的 JSON 格式會把應該為「數值」的數字資料使用「字串」的格式來表達，這導致他在使用 [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json?view=net-6.0&WT.mc_id=DT-MVP-4015686) 的 `JsonSerializer.Deserialize` 進行反序列化時出現錯誤。這篇文章我來分享一個鮮為人知的小秘訣，讓你輕鬆駕馭各種 Web 常見的 JSON 格式。

![JSON](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/55572c6a834d0c434e48-199890654-c1752f91-1034-48ab-96bb-0c3c05efb18d.png)

### 問題說明

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

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

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

```csharp
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](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializer.deserialize?source=recommendations&view=net-6.0&WT.mc_id=DT-MVP-4015686) 方法來進行反序列化操作：

```csharp
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.](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/9ab39a5ee713984ec6bd-199883427-df9f7ebf-fced-4b4c-be1e-c784cf55060b.png)

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

### 解決方案

要解決這個問題，主要有兩種：

1.  自訂轉換器 ([How to write custom converters for JSON serialization (marshalling) in .NET](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-6-0&WT.mc_id=DT-MVP-4015686))
    
    自訂轉換器要額外定義一個類別，雖然結構不複雜，但實作上有比較麻煩些。
    
2.  使用 `System.Text.Json` 內建的 `JsonSerializerDefaults.Web` 序列化選項
    
    這個解決方案真的出乎意外的簡單，多加一個參數即可：
    
    ```csharp
    var user = JsonSerializer.Deserialize<UserProfile>(jsonText,
                  new JsonSerializerOptions(JsonSerializerDefaults.Web));
    ```
    
    > 注意: [JsonSerializerDefaults 列舉](https://learn.microsoft.com/zh-tw/dotnet/api/system.text.json.jsonserializerdefaults?view=net-6.0&WT.mc_id=DT-MVP-4015686)是從 .NET 5 才開始有的型別。
    

### 認識 `JsonSerializerDefaults.Web` 列舉

你要是直接翻出 `JsonSerializerOptions` 建構式的[原始碼](https://source.dot.net/#System.Text.Json/System/Text/Json/Serialization/JsonSerializerOptions.cs,e62920980b159ed0)出來看，就會發現箇中奧妙之處：

```csharp
/// <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](https://learn.microsoft.com/zh-tw/dotnet/api/system.text.json.jsonserializerdefaults?view=net-6.0&WT.mc_id=DT-MVP-4015686) 列舉(Enum)的 `JsonSerializerDefaults.Web` 定義了三個序列化的選項(特性)，其中包括：

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

> 💡 [查看 JsonSerializerDefaults 原始碼](https://source.dot.net/#System.Text.Json/System/Text/Json/Serialization/JsonSerializerDefaults.cs,8957b03f0456bd22)

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

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

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

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

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

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

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

### 相關連結

-   [JsonSerializerOptions 類別 (System.Text.Json) | Microsoft Learn](https://learn.microsoft.com/zh-tw/dotnet/api/system.text.json.jsonserializeroptions?view=net-6.0&WT.mc_id=DT-MVP-4015686)
-   [JsonSerializerDefaults 列舉 (System.Text.Json) | Microsoft Learn](https://learn.microsoft.com/zh-tw/dotnet/api/system.text.json.jsonserializerdefaults?view=net-6.0&WT.mc_id=DT-MVP-4015686)
-   [How to write custom converters for JSON serialization (marshalling) in .NET](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-6-0&WT.mc_id=DT-MVP-4015686)
