CodeGym /课程 /C# SELF /通过属性控制序列化过程

通过属性控制序列化过程

C# SELF
第 45 级, 课程 2
可用

1. 为什么序列化时需要属性?

当你把对象序列化成 JSON 或 XML 时,发生的事情有点像用清洁机器人“扫描”你的房间。所有明显放在外面的东西(public 属性/字段)会被放进“袋子”(文件或字符串),其他的会被忽略。但有时候你不想让机器人展示你包里的东西,或者你想把东西换个更合适的名字——比如用英文而不是俄文。

这时就用到属性了!它们让你能控制序列化过程:隐藏不想暴露的内容、改名、设定顺序、忽略对象的某些部分,或者告诉序列化器有特殊规则。就像给物品贴标签:“别动”、“重要”、“在 JSON 中重命名”。

常见的序列化控制需求

  • 在输出文档中改变属性或字段的名字(比如在 JSON 中把 FirstName 变成 "first_name")
  • 从序列化或反序列化中隐藏某些字段/属性(比如密码、内部计数器)
  • 控制默认值或 null 值的处理方式
  • 指定元素顺序(对 XML 很重要)
  • 为 XML 元素描述额外参数(attributes)

每个平台的序列化器——比如 System.Text.Json、Newtonsoft.Json 或 XmlSerializer——都有自己的属性来完成这些事。

2. System.Text.Json 的属性:现代又流行

.NET 的经典 JSON 序列化器支持一组不错的属性,这些属性在命名空间 System.Text.Json.Serialization 下。

最有用的属性:

属性 用途 示例
JsonPropertyName("...")
把属性名转换为 JSON 中的名字
[JsonPropertyName("id")]
JsonIgnore
完全从序列化中排除该属性
[JsonIgnore]
JsonInclude
序列化公共字段(不仅仅是属性)
[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
}

我们来序列化这样一个人:

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
可以指定自定义转换器来处理复杂情况
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 被序列化成了属性,而不是嵌套的标签。

5. 有用的小细节

内部原理:属性是怎么工作的

当序列化器遇到你的类时,它会通过反射“读”这个类(.NET 的魔法,详见 System.Reflection)。序列化器会读取元数据:比如属性上有没有 JsonIgnore 或 XmlElement。根据这些信息,它决定把哪些数据加入最终文档,或者跳过哪些数据。

这是把“数据模式”与业务逻辑分离的便捷方式。你的类是业务逻辑,属性其实就是给序列化的“护照”。

关键属性对照表

功能 System.Text.Json Newtonsoft.Json XmlSerializer
改名
[JsonPropertyName]
[JsonProperty]
[XmlElement]
[XmlAttribute]
忽略
[JsonIgnore]
[JsonIgnore]
[XmlIgnore]
自定义格式
[JsonConverter]
[JsonConverter]
—(通过 IXmlSerializable,比较麻烦)
根对象 — —
[XmlRoot]
集合 — —
[XmlArray]
[XmlArrayItem]

6. 高级场景

有时候你需要的序列化方式超出标准序列化器能做的范围。比如你想把日期存成 UNIX timestamp 而不是 ISO 字符串。这种情况可以用属性指定自定义转换器。

在 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",反序列化时会变回 true 或 false。

7. 特点和坑

当你加属性时记住:不同序列化器用不同的属性。如果你同时处理 JSON 和 XML(或者同时用 Newtonsoft.Json 和 System.Text.Json),别忘了同时加上两个相应的属性,否则会有惊喜。

如果你不通过属性覆盖,序列化器会使用默认的属性名。

注意继承:子类会继承父类的 public 字段/属性,如果父类上有属性,通常也会作用到子类上。这经常让人困惑,但这是设计如此。

常见错误:常见的情况是开发者不小心给一个必须序列化的字段贴上了 JsonIgnore(或者相反——忘了忽略敏感数据)。比如把 XmlElement 标到 private 字段上,然后奇怪为什么序列化器看不到它(XmlSerializer 只处理 public 成员!)。

2
任务
C# SELF, 第 45 级, 课程 2
已锁定
忽略 null 值
忽略 null 值
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION