CodeGym /課程 /C# SELF /深入探討 System.Text.Json

深入探討 System.Text.Json

C# SELF
等級 47, 課堂 1
開放

1. 介紹

在這堂課我們會從 "只要幫我把列表存起來就好" 這種需求,走到使用 System.Text.Json 做靈活、精確且高效的序列化。 在現代的 .NET 專案裡,JSON 幾乎是資料交換的事實標準。基本的呼叫 Serialize/Deserialize 很簡單,但實際問題通常需要細節設定:忽略/重新命名欄位、管理日期格式、保護循環參考、處理大量資料、自訂 converters 等等。

我們會不只看 JsonSerializer 的方法,還會看如何透過 JsonSerializerOptions 設定、使用屬性、跟 Stream 搭配、記憶體管理,以及透過 JsonConverter 注入自訂的序列化規則。

簡短歷史與定位 System.Text.Json

長期以來 .NET 由 Newtonsoft.Json (Json.NET) 主導 — 功能強大且成熟,但不一定是最快或最輕量的外部依賴。從 .NET Core 3.0 開始出現內建的 System.Text.Json:性能高、依賴少(平台內建)、跟 ASP.NET Core 整合緊密,且隨著 .NET 版本持續演進。

2. 主要類別與方法

基本單位是靜態類別 JsonSerializer,它提供兩個方向:

  • 序列化:物件 → JSON 字串 (Serialize)
  • 反序列化:JSON 字串 → 指定類型的物件 (Deserialize)

簡單物件的序列化範例

using System.Text.Json;

var person = new Person { Name = "伊凡", Age = 30 };
string jsonString = JsonSerializer.Serialize(person);
Console.WriteLine(jsonString); // {"Name":"伊凡","Age":30}

反序列化範例

var json = "{\"Name\":\"安娜\",\"Age\":22}";
var anna = JsonSerializer.Deserialize<Person>(json);
Console.WriteLine(anna.Name); // 安娜

備註:型別 Person 已在之前的講義實作過 — 這裡沿用。

3. 序列化控制:JsonSerializerOptions

在真實專案幾乎總會需要設定:屬性名稱用 camelCase、日期格式、循環處理、預設值等。這些都可以透過 JsonSerializerOptions 來控制。

設定範例

var options = new JsonSerializerOptions
{
    WriteIndented = true,    // 美化 JSON(加入空白和換行)
    PropertyNameCaseInsensitive = true, // 反序列化時忽略屬性名稱大小寫
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase // 屬性使用 camelCase(不是 PascalCase)
};

string json = JsonSerializer.Serialize(person, options);
/*
{
  "name": "伊凡",
  "age": 30
}
*/

為什麼重要?大多數前端框架預期使用 camelCase,不是 .NET 預設的 PascalCase。

4. 屬性(attributes):System.Text.Json.Serialization

有時候直接在模型上用屬性控制序列化會比較方便。可以把屬性加在欄位/屬性上,影響名稱、包含/排除行為、值的處理方式。

主要屬性

屬性 功能
[JsonIgnore]
把屬性從序列化/反序列化排除
[JsonPropertyName("名稱")]
在 JSON 中使用不同的名稱
[JsonInclude]
將非公開的屬性/欄位包含進序列化
[JsonNumberHandling]
控制數字值的處理方式

範例:用屬性控制屬性行為

using System.Text.Json.Serialization;

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

    [JsonIgnore]
    public int SecretCode { get; set; }

    public int Age { get; set; }
}
var person = new Person { Name = "彼得", Age = 45, SecretCode = 123 };
string json = JsonSerializer.Serialize(person);
// {"full_name":"彼得","Age":45}

注意:SecretCode 沒出現在 JSON 中,而 Name 則以 "full_name" 序列化。

5. 集合與巢狀物件的序列化

集合其實很簡單

var numbers = new List<int> { 1, 2, 3 };
string json = JsonSerializer.Serialize(numbers); // [1,2,3]

var people = new List<Person> {
    new Person { Name = "安娜", Age = 20 },
    new Person { Name = "馬克西姆", Age = 40 }
};
string jsonList = JsonSerializer.Serialize(people);
// [{"Name":"安娜","Age":20},{"Name":"馬克西姆","Age":40}]

巢狀結構

public class Group
{
    public string Name { get; set; }
    public List<Person> Members { get; set; }
}

var group = new Group
{
    Name = "開發者",
    Members = new List<Person>
    {
        new Person { Name = "薩沙", Age = 23 },
        new Person { Name = "瑪莎", Age = 28 }
    }
};

string jsonGroup = JsonSerializer.Serialize(group, options);
/*
{
  "name": "開發者",
  "members": [
    { "name": "薩沙", "age": 23 },
    { "name": "瑪莎", "age": 28 }
  ]
}
*/

6. 反序列化:重要事項

var json = "[{\"Name\":\"伊凡\",\"Age\":21}]";
var list = JsonSerializer.Deserialize<List<Person>>(json);
Console.WriteLine(list[0].Name); // 伊凡

常見情況:如果 JSON 裡少了某個欄位,對應的屬性會拿到預設值。JSON 裡有多餘的欄位但模型沒有,這些欄位會被忽略。但如果型別不匹配(例如原本應該是數字卻收到字串),反序列化時會拋出例外。

7. 日期、時間、格式與數值處理

public class Meeting
{
    public string Topic { get; set; }
    public DateTime Time { get; set; }
}

var meeting = new Meeting { Topic = "會議", Time = DateTime.Now };
string json = JsonSerializer.Serialize(meeting);
// {"Topic":"會議","Time":"2024-06-06T20:30:00.0000000+03:00"}

預設情況下 DateTime 會以 ISO 8601 格式序列化。需要不同的格式(例如只要日期)時,可以用額外的屬性或自訂的 converter(見下方)。

FAQ:如果想把數字序列化成字串(例如電話號碼或超大 ID),可以使用屬性 [JsonNumberHandling(JsonNumberHandling.WriteAsString)]。

8. Stream 與檔案操作

可以不只用字串操作,還能用 Stream — 這對大量資料(檔案、網路)很重要。

寫入檔案範例

using var fs = File.Create("person.json");
JsonSerializer.Serialize(fs, person);
// 別忘了呼叫 fs.Flush() 或使用 using!

從檔案讀取範例

using var fs = File.OpenRead("person.json");
var restored = JsonSerializer.Deserialize<Person>(fs);

對 Stream 也有非同步方法 SerializeAsync/DeserializeAsync — 對高負載服務很有用。

9. 自訂 converters

如果預設規則不夠(非標準的日期/數字格式、複雜值、自訂結構)— 就自己寫 JsonConverter。

範例:只用 "dd.MM.yyyy" 的日期

public class CustomDateConverter : JsonConverter<DateTime>
{
    public override DateTime Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return DateTime.ParseExact(reader.GetString(), "dd.MM.yyyy", null);
    }

    public override void Write(
        Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("dd.MM.yyyy"));
    }
}

var options = new JsonSerializerOptions();
options.Converters.Add(new CustomDateConverter());

var dt = new DateTime(2024, 6, 1);
string json = JsonSerializer.Serialize(dt, options); // "01.06.2024"

自訂 converter 很適合序列化座標、向量、顏色、非標準日期和貨幣、各種 ID 格式等等。

10. 實用細節

循環參考與深層階層的處理

var options = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.Preserve, // 保留物件並用 $id/$ref 表示
    WriteIndented = true
};

重要:JSON 會多出輔助屬性 $id 與 $ref。若要跟不支援這些屬性的外部系統交換資料,這種方式可能不適合。

System.Text.Json 與 Newtonsoft.Json 的差異

System.Text.Json 已經很強,但還不是完全涵蓋所有 Newtonsoft.Json 的場景(例如私有建構子、複雜的動態物件等等)。對大多數常見需求我們建議用內建的序列化器 — 它更快且沒有額外依賴。

以互動方式操作 JSON:DOM API

當你不想建立完整模型就要遍歷 JSON 時,可以用 JsonDocument 與 JsonElement。

using var doc = JsonDocument.Parse(jsonString);
JsonElement root = doc.RootElement;

if (root.TryGetProperty("Name", out var nameProperty))
{
    Console.WriteLine(nameProperty.GetString());
}

11. 有用的選項與它們的效果

屬性 說明/用途
WriteIndented
true — 用縮排格式化輸出
PropertyNameCaseInsensitive
true — 反序列化時忽略屬性名稱大小寫
PropertyNamingPolicy
JsonNamingPolicy.CamelCase
DefaultIgnoreCondition
控制對 null/預設值的忽略規則
ReferenceHandler
Preserve
,
IgnoreCycles
AllowTrailingCommas
true — 允許陣列或物件結尾有多餘的逗號
NumberHandling
數字與字串之間的轉換處理(等等)
Converters
自訂 converters 的清單

12. 常見錯誤與實務建議

錯誤 #1:反序列化到錯誤的型別。 如果你序列化的是集合,就要反序列化回集合:List<T>,不要反序列化成單一物件。

錯誤 #2:屬性名稱大小寫不對。 若沒設定大小寫不敏感,屬性可能會「找不到」。使用 PropertyNameCaseInsensitive 或調整 PropertyNamingPolicy。

錯誤 #3:日期處理不正確。 預設格式是 ISO 8601。需要不同格式時,建立並使用自訂 converter(JsonConverter<DateTime>)。

錯誤 #4:期待序列化私有/靜態欄位。 預設只會處理公開的屬性。特殊需求用對應屬性(例如 [JsonInclude])。

錯誤 #5:不了解預設值的行為。 JSON 缺少欄位 → 屬性拿到預設值。設計邏輯時要注意這點。

錯誤 #6:Stream 管理不當。 用 using 或 await using 來關閉資源,避免記憶體洩漏或鎖檔。

2
任務
C# SELF, 等級 47, 課堂 1
上鎖
使用 JsonSerializerOptions 設定序列化物件
使用 JsonSerializerOptions 設定序列化物件
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION