CodeGym /課程 /C# SELF /透過屬性控制序列化過程

透過屬性控制序列化過程

C# SELF
等級 45 , 課堂 2
開放

1. 為什麼在序列化時需要屬性?

當你把物件序列化成 JSON 或 XML 時,發生的情況有點像一台掃地機器人在打掃你的房間。所有明顯擺在外面的東西(public 屬性/欄位)都會被「裝進袋子」(寫進檔案或字串),其他的則被忽略。但有時你不想讓機器人露出你包包裡的內容,或想把東西改個更好理解的名字——例如,不用俄文而用英文。

這時屬性就登場了!屬性讓你能控制序列化流程:隱藏不需要的資料、修改名稱、標註順序、忽略物件的某些部分,或告訴序列化器你有特殊規則。就像貼在東西上的貼紙:不要動、很重要、在 JSON 中改名。

常見的序列化控制任務

  • 在輸出檔案中改變屬性或欄位的名稱(例如,把 FirstName 變成 JSON 裡的 "first_name"
  • 在序列化或反序列化時隱藏某些欄位/屬性(比如密碼、內部計數器)
  • 控制預設值或 null 值的處理方式
  • 指定元素的順序(對 XML 特別重要)
  • 為 XML 元素描述額外參數(attributes)

每個序列化平台——不論是 System.Text.JsonNewtonsoft.Json 還是 XmlSerializer——都有自己的一組屬性來完成這些任務。

2. System.Text.Json 的屬性:現代又流行

.NET 的內建 JSON 序列化器支援相當實用的一系列屬性,這些屬性位於命名空間 System.Text.Json.Serialization

最有用的屬性:

屬性 用途 使用範例
JsonPropertyName("...")
將屬性名稱轉換成 JSON 裡的名稱
[JsonPropertyName("id")]
JsonIgnore
完全排除該屬性不參與序列化
[JsonIgnore]
JsonInclude
包含 public 欄位的序列化(不只針對屬性)
[JsonInclude]
JsonIgnore(Condition = ...)
在特定條件下忽略該屬性
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]

使用範例:

using System.Text.Json.Serialization;

public class Person
{
    [JsonPropertyName("first_name")]
    public string FirstName { get; set; } // 在 JSON 裡的名稱會是 'first_name'

    [JsonIgnore]
    public string Password { get; set; } // 不會出現在 JSON

    [JsonPropertyName("born_year")]
    public int? YearOfBirth { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Nickname { get; set; } // 如果是 null 就不會出現在 JSON
}

我們來序列化這個 person:

var person = new Person
{
    FirstName = "Ivan",
    Password = "123456",
    YearOfBirth = 2000,
    Nickname = null
};

string json = JsonSerializer.Serialize(person);
// json: {"first_name":"Ivan","born_year":2000}

注意:密碼和暱稱都沒有出現在 JSON!(密碼是因為永遠被忽略,暱稱則是在它是 null 時被忽略)。

為什麼這很重要? 實務上你通常不希望前端(或壞人)看到像密碼、token、計數器、內部時間戳等敏感資料。用 [JsonIgnore] 這類屬性可以一行搞定。

3. Newtonsoft.Json 的屬性:靈活的旗艦

如果你使用 Newtonsoft.Json(官方文件在這裡),可用的功能更多,對於舊專案也比較友善。

主要的屬性:

屬性 用途
JsonProperty("...")
設定 JSON 裡的屬性名稱,還能設定順序、是否必要等
JsonIgnore
將欄位/屬性排除在序列化之外
JsonRequired
反序列化時要求該欄位一定存在
JsonConverter
允許指定自訂 converter 處理複雜情況
DefaultValueHandling
控制預設值的序列化方式

實作範例:

using Newtonsoft.Json;

public class UserProfile
{
    [JsonProperty("login")]
    public string Username { get; set; }

    [JsonIgnore]
    public string InternalNotes { get; set; }

    [JsonProperty(Required = Required.Always)]
    public string Email { get; set; }
}

JsonProperty 還有更多選項——例如 Required(必要性)、Order(順序)等等。例如:

[JsonProperty("id", Order = 1, Required = Required.Always)]
public int Id { get; set; }

4. XML 的屬性:經典風格

XmlSerializer 有自己一套屬性,位於命名空間 System.Xml.Serialization

常見屬性:

屬性 用途
[XmlElement("...")]
在 XML 中作為一個元素,並使用不同的名稱
[XmlAttribute("...")]
把屬性轉成 XML 元素的 attribute
[XmlIgnore]
排除該屬性或欄位不做 XML 序列化
[XmlArray("...")]
對集合指定 XML 陣列的名稱
[XmlArrayItem("...")]
對集合指定陣列中單一項目的名稱
[XmlRoot("...")]
改變 XML 根標籤的名稱

實作範例:

using System.Xml.Serialization;

[XmlRoot("human")]
public class Person
{
    [XmlElement("firstname")]
    public string Name { get; set; }

    [XmlAttribute("years")]
    public int Age { get; set; }

    [XmlIgnore]
    public string Secret { get; set; }
}

var person = new Person { Name = "Anna", Age = 32, Secret = "42" };

序列化後的 XML 大致會像這樣:

<human years="32"><firstname>Anna</firstname></human>

注意 Secret 沒有出現在 XML 裡,而 Age 被序列化為 attribute,而不是內嵌的 tag。

5. 有用的細節

底層運作:屬性如何生效

當序列化器遇到你的類別時,它會透過反射去「讀」你的型別(.NET 的 magic,詳見 System.Reflection)。序列化器會讀取元資料:例如屬性上有沒有 JsonIgnoreXmlElement。根據這些資訊,它會決定把哪些資料加入輸出文件,或者跳過哪些欄位。

這是把「資料結構描述」和業務邏輯分離的一個方便方式。你的類別是業務邏輯,而屬性本質上就是序列化的說明文件。

關鍵屬性對照表

功能 System.Text.Json Newtonsoft.Json XmlSerializer
改名
[JsonPropertyName]
[JsonProperty]
[XmlElement]
[XmlAttribute]
忽略
[JsonIgnore]
[JsonIgnore]
[XmlIgnore]
自訂格式
[JsonConverter]
[JsonConverter]
—(透過 IXmlSerializable,比較麻煩)
物件根
[XmlRoot]
集合
[XmlArray]
[XmlArrayItem]

6. 進階情境

有時你需要的序列化行為不是標準序列化器預設的。例如你想把日期以 UNIX timestamp 存,而不是一般的 ISO 字串。這時你可以用屬性掛上自訂 converter。

System.Text.Json 中:

public class UnixDateTimeConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64()).UtcDateTime;

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        => writer.WriteNumberValue(new DateTimeOffset(value).ToUnixTimeSeconds());
}

public class LogEntry
{
    [JsonConverter(typeof(UnixDateTimeConverter))]
    public DateTime EventTime { get; set; }
}

現在序列化時 EventTime 會變成一個數字,而不是日期字串。

Newtonsoft.Json 中:

public class BoolToYesNoConverter : JsonConverter<bool>
{
    public override void WriteJson(JsonWriter writer, bool value, JsonSerializer serializer)
        => writer.WriteValue(value ? "yes" : "no");

    public override bool ReadJson(JsonReader reader, Type objectType, bool existingValue, bool hasExistingValue, JsonSerializer serializer)
        => (string)reader.Value == "yes";
}

public class Answer
{
    [JsonConverter(typeof(BoolToYesNoConverter))]
    public bool IsCorrect { get; set; }
}

現在布林值會被序列化成 "yes""no",反序列化回來則變成 truefalse

7. 特點與坑

當你開始使用屬性時,記得:不同的序列化器使用不同的屬性。如果你同時處理 JSON 與 XML(或同時用到 Newtonsoft.JsonSystem.Text.Json),不要忘了同一個欄位可能需要標上兩個不同的屬性,否則會有驚喜。

如果你沒有用屬性覆寫,序列化器會採用預設的屬性名稱。

繼承要小心:子類別會繼承父類別的 public 欄位/屬性,如果父類別上有屬性,這些元資料也會套用到子類。這經常讓人覺得奇怪,但這是預期行為。

常見錯誤: 很多人不小心把本該要序列化的重要欄位標成 JsonIgnore(或反過來忘了忽略敏感資料)。或是把 XmlElement 標到 private 欄位上,然後納悶為什麼序列化器看不到它(XmlSerializer 只處理 public 成員!)。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION