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
有時候直接在模型上用屬性控制序列化會比較方便。可以把屬性加在欄位/屬性上,影響名稱、包含/排除行為、值的處理方式。
主要屬性
| 屬性 | 功能 |
|---|---|
|
把屬性從序列化/反序列化排除 |
|
在 JSON 中使用不同的名稱 |
|
將非公開的屬性/欄位包含進序列化 |
|
控制數字值的處理方式 |
範例:用屬性控制屬性行為
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. 有用的選項與它們的效果
| 屬性 | 說明/用途 |
|---|---|
|
true — 用縮排格式化輸出 |
|
true — 反序列化時忽略屬性名稱大小寫 |
|
|
|
控制對 null/預設值的忽略規則 |
|
, |
|
true — 允許陣列或物件結尾有多餘的逗號 |
|
數字與字串之間的轉換處理(等等) |
|
自訂 converters 的清單 |
12. 常見錯誤與實務建議
錯誤 #1:反序列化到錯誤的型別。 如果你序列化的是集合,就要反序列化回集合:List<T>,不要反序列化成單一物件。
錯誤 #2:屬性名稱大小寫不對。 若沒設定大小寫不敏感,屬性可能會「找不到」。使用 PropertyNameCaseInsensitive 或調整 PropertyNamingPolicy。
錯誤 #3:日期處理不正確。 預設格式是 ISO 8601。需要不同格式時,建立並使用自訂 converter(JsonConverter<DateTime>)。
錯誤 #4:期待序列化私有/靜態欄位。 預設只會處理公開的屬性。特殊需求用對應屬性(例如 [JsonInclude])。
錯誤 #5:不了解預設值的行為。 JSON 缺少欄位 → 屬性拿到預設值。設計邏輯時要注意這點。
錯誤 #6:Stream 管理不當。 用 using 或 await using 來關閉資源,避免記憶體洩漏或鎖檔。
GO TO FULL VERSION