CodeGym /课程 /C# SELF /深入了解 System.Text.Json

深入了解 System.Text.Json

C# SELF
第 47 级, 课程 1
可用

1. 介绍

在本节我们从"就给我保存个列表"迈向灵活、精确且高性能的序列化,使用 System.Text.Json。在现代 .NET 项目里 JSON 已经是事实上的数据交换标准。基础的 Serialize/Deserialize 调用很简单,但真实场景需要细致的配置:忽略/重命名字段、控制日期格式、防止循环、处理大数据量、自定义转换器等等。

我们不仅会看 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. 属性: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。如果需要别的形式(比如只要日期)?可以用单独属性或自定义转换器(见下文)。

常见问题解答:如果想把数字以字符串形式序列化(比如电话号码或大 ID),可以使用属性 [JsonNumberHandling(JsonNumberHandling.WriteAsString)]。

8. 流与文件操作

可以不仅仅对字符串操作,还可以用 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);

对于流也有异步方法 SerializeAsync/DeserializeAsync — 对高并发服务很有用。

9. 自定义转换器

当默认规则不够用(非标准的日期/数字格式、复杂值、自定义结构)时,编写 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"

自定义转换器很适合序列化坐标、向量、颜色、非标准日期和货币、各种 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 来“遍历”JSON。

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
自定义转换器列表

12. 常见错误与实用建议

错误 №1:反序列化到错误的类型。 如果你序列化的是集合,就要反序列化为集合:List<T>,而不是单个对象。

错误 №2:属性名大小写不对。 没有配置大小写不敏感时,属性可能“找不到”。使用 PropertyNameCaseInsensitive 或设置 PropertyNamingPolicy。

错误 №3:日期处理不正确。 默认格式是 ISO 8601。需要别的格式就写并应用转换器(JsonConverter<DateTime>)。

错误 №4:期待序列化私有/静态字段。 默认只处理公共属性。非标准情况使用相应属性(比如 [JsonInclude])。

错误 №5:误解默认值。 JSON 中缺失字段 → 属性得到默认值。在业务逻辑里要考虑到这一点。

错误 №6:流管理不当。 用 using 或 await using 关闭资源,避免泄露和阻塞。

2
任务
C# SELF, 第 47 级, 课程 1
已锁定
使用 JsonSerializerOptions 设置序列化对象
使用 JsonSerializerOptions 设置序列化对象
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION