1. 介绍
长期以来,在 .NET 生态里,处理 JSON 的主要工具是很棒的第三方包 Newtonsoft.Json(也叫 Json.NET)。它功能强大、灵活,直到现在仍被广泛使用。但随着 .NET 9 和 C# 14 的到来,Microsoft 觉得应该有一个内置的高性能 JSON 序列化器。于是就有了 System.Text.Json。
为什么需要新的方案?System.Text.Json 是为现代场景设计的,解决了多年使用第三方库积累的问题。它针对最高性能和安全性做了优化,适合异步场景和 web-API,而且 — 最棒的是 — 不需要通过 NuGet 安装:平台里自带。
当然,Newtonsoft.Json 还在,并且我们会在后面提到它。但对大多数新项目来说,System.Text.Json 是默认选择。准备好吧,现在我们要教对象说 JSON 语言了!
2. 使用 System.Text.Json 的基础
简单的对象序列化
好,先从基础魔法开始。把对象序列化成 JSON 字符串。
using System;
using System.Text.Json; // 别忘了加上!
public class Player
{
public string Name { get; set; }
public int Health { get; set; }
public bool IsAlive { get; set; }
}
// 在程序的某处:
Player player = new Player { Name = "Aragorn", Health = 100, IsAlive = true };
// 序列化:
string json = JsonSerializer.Serialize(player);
Console.WriteLine(json); // 输出: {"Name":"Aragorn","Health":100,"IsAlive":true}
备注: 如果你刚开始学序列化,这个例子说明了有多简单:JsonSerializer.Serialize — 就搞定了。
对象反序列化
从 JSON 字符串把对象“复活”出来:
string incomingJson = "{\"Name\":\"Legolas\",\"Health\":88,\"IsAlive\":true}";
Player player2 = JsonSerializer.Deserialize<Player>(incomingJson);
Console.WriteLine(player2.Name); // Legolas
Console.WriteLine(player2.Health); // 88
Console.WriteLine(player2.IsAlive); // true
备注: 如果 JSON 的结构和你的类一致 — 一切都会像钟表一样工作。否则可能会抛异常或得到默认值。
3. 序列化的工作原理和机制
如何进行匹配
System.Text.Json 默认使用类里相同的属性名。区分大小写!如果 JSON 里写了 health 而不是 Health,反序列化就不会匹配 — 属性会保持默认值(0、false 或 null)。
例如:
// 小写键的 JSON:
string badJson = "{\"name\":\"Gimli\",\"health\":120,\"isAlive\":true}";
Player player3 = JsonSerializer.Deserialize<Player>(badJson);
Console.WriteLine(player3.Name); // 空
Console.WriteLine(player3.Health); // 0
Console.WriteLine(player3.IsAlive); // false
有趣的事实: 许多 API 使用 camelCase(比如 health),而在 C# 里通常用 PascalCase(比如 Health)。这可以通过下面的设置来解决。
4. 控制序列化 — 选项和配置
格式化 JSON:可读的输出
有时候你想要不是紧凑的,而是美观的 JSON — 用于配置或日志。
var options = new JsonSerializerOptions
{
WriteIndented = true // 添加缩进
};
string prettyJson = JsonSerializer.Serialize(player, options);
Console.WriteLine(prettyJson);
/*
{
"Name": "Frodo",
"Health": 50,
"IsAlive": true,
"Inventory": [
"Ring",
"Bread",
"Torch"
],
"Position": {
"X": 5,
"Y": 15
}
}
*/
通过设置 WriteIndented,序列化器会加入缩进和换行。
控制命名风格(CamelCase vs PascalCase)
如果你在做的 web-API 全部键都是 camelCase,可以开启命名策略:
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string camelCaseJson = JsonSerializer.Serialize(player, options);
// {"name":"Frodo","health":50,"isAlive":true,"inventory":["Ring","Bread","Torch"],"position":{"x":5,"y":15}}
这样在反序列化时这些键就能正确匹配:
string apiJson = "{\"name\":\"Bilbo\",\"health\":40,\"isAlive\":true,\"inventory\":[\"Mug\"],\"position\":{\"x\":10,\"y\":5}}";
Player bilbo = JsonSerializer.Deserialize<Player>(apiJson, options);
Console.WriteLine(bilbo.Name); // Bilbo
5. 有用的细节
使用属性 [JsonIgnore]
有时候不想序列化所有属性 — 比如私密数据或临时计算值。
using System.Text.Json.Serialization;
public class Player
{
public string Name { get; set; }
public int Health { get; set; }
[JsonIgnore] // 这个属性不会出现在 JSON 里
public bool IsSecretCharacter { get; set; }
}
现在序列化时:
var player = new Player { Name = "Boromir", Health = 80, IsSecretCharacter = true };
string json = JsonSerializer.Serialize(player);
Console.WriteLine(json); // {"Name":"Boromir","Health":80}
反序列化时 IsSecretCharacter 会得到默认值(false)。
使用 [JsonPropertyName("...")]
假设代码里属性叫 IsAlive,但在 JSON 里需要是 "status":
using System.Text.Json.Serialization;
public class Player
{
public string Name { get; set; }
public int Health { get; set; }
[JsonPropertyName("status")]
public bool IsAlive { get; set; }
}
序列化后的样子:
var player = new Player { Name = "Pippin", Health = 60, IsAlive = false };
string json = JsonSerializer.Serialize(player);
Console.WriteLine(json); // {"Name":"Pippin","Health":60,"status":false}
反序列化时,键 "status" 会正确填充属性 IsAlive。
内置的限制和安全特性
- 默认只序列化带有 public getter/setter 的公共属性;私有字段/属性会被忽略。
- 遇到循环引用会抛出异常: "A possible object cycle was detected"。
- 和 Newtonsoft.Json 不同,标准序列化器更少依赖对类型的“魔术”处理 — 但在典型场景下更安全也更快。
6. 常见错误和坑
你不小心改了 JSON 里的属性名但忘了更新代码 — 结果属性得到默认值(null、0、false)。
JSON 缺少必需字段 — 对应的对象属性会是默认值(参见 文档)。
属性没有 public setter — 反序列化时它不会被填充。
更改了嵌套类或集合的结构 — 反序列化可能会失败或产生意外结果。
在不同嵌套层级出现相同名字(父对象和子对象里)会让人困惑,调试也难。
有时问题跟平台版本有关:旧版本的 System.Text.Json 在处理某些类型(比如 Dictionary、DateTime、enum)时表现不佳,但在 .NET 7/8/9 中很多问题已经修复。
GO TO FULL VERSION