编辑
2026-04-10
C#
00

目录

🔍 为什么要关注 System.Text.Json?
📊 先看一组真实数据对比
⚠️ 常见的三个误解
💡 核心知识点拆解
🎯 三种序列化模式的选择
🔑 必须掌握的配置选项
🚀 渐进式解决方案实战
方案一:快速上手 - 基础序列化与配置
方案二:进阶玩法 - 自定义转换器
方案三:性能极致 - Source Generator
方案四:实战场景 - 流式处理大文件
🎓 三个关键设计原则
1️⃣ 配置复用原则
2️⃣ 异常边界原则
3️⃣ 性能监控原则
🎯 三句话总结
💬 互动话题
🏷️ 相关标签

遇到过一个让人头疼的问题:高峰期API响应慢得像老牛拉车,排查后发现JSON序列化竟然占了30%的CPU时间!当时项目里用的是老牌的Newtonsoft.Json,虽然功能强大,但在高并发场景下确实有点吃不消。

后来切换到 System.Text.Json 后,序列化性能提升了 2-3倍,内存分配减少了 40% 左右(基于. NET 6测试环境,10万次序列化操作)。这篇文章咱们就聊聊这个微软官方钦定的JSON库,它不仅仅是"又一个JSON库"那么简单。

读完本文你能收获:

  • 掌握System.Text.Json的核心用法与配置技巧
  • 学会编写自定义转换器解决复杂场景
  • 获得3个可直接落地的性能优化方案
  • 避开95%开发者会踩的常见坑

🔍 为什么要关注 System.Text.Json?

📊 先看一组真实数据对比

我在本地做了个简单测试(环境:. NET 8, Release模式):

image.png

⚠️ 常见的三个误解

很多同学跟我说过类似的疑虑,咱们得先破除这些误区:

  1. 误解一:"功能没Newtonsoft全"
    确实,某些特殊场景(比如DataSet序列化)确实支持不够完善,但80%的常规需求都能覆盖,而且微软在持续更新。

  2. 误解二:"迁移成本太高"
    其实大部分代码改动就是换个命名空间,核心逻辑基本不动。我团队去年迁移了一个20万行的项目,实际改动代码不到300行。

  3. 误解三:"只适合新项目"
    老项目也能渐进式迁移,两个库可以共存。我见过不少项目是先把性能敏感模块(比如日志、缓存)换成Text.Json,然后逐步扩大范围。

💡 核心知识点拆解

🎯 三种序列化模式的选择

System.Text.Json提供了三种主要方式,每种都有最佳适用场景:

  1. JsonSerializer(反射模式):快速开发,性能中等
  2. JsonSerializerOptions(配置优化):平衡灵活性与性能
  3. Source Generator(编译时生成):极致性能,零反射

我的建议是:原型阶段用反射,生产环境上Source Generator。就像开车,先学自动挡,熟练了再开手动挡追求极致控制。

🔑 必须掌握的配置选项

这几个配置选项在实战中用得最多,我按使用频率排个序:

csharp
var options = new JsonSerializerOptions { // 🔥 使用频率Top1:属性命名策略(前端对接必备) PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // 🔥 Top2:美化输出(调试神器,生产环境记得关) WriteIndented = true, // 🔥 Top3:忽略null值(减少传输体积) DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 🔥 Top4:允许尾随逗号(兼容手写JSON) AllowTrailingCommas = true, // 🔥 Top5:大小写不敏感(容错处理) PropertyNameCaseInsensitive = true };

🚀 渐进式解决方案实战

方案一:快速上手 - 基础序列化与配置

适用场景: 新手入门、API接口开发、配置文件读写

先看最常见的用法,我刚带的实习生第一天就能上手:

csharp
using System.Text.Json; using System.Text.Json.Serialization; // 定义数据模型 public class Product { public int Id { get; set; } // 自定义JSON属性名 [JsonPropertyName("product_name")] public string Name { get; set; } // 忽略某个属性 [JsonIgnore] public string InternalCode { get; set; } public decimal Price { get; set; } public DateTime CreatedAt { get; set; } } // 序列化示例 var product = new Product { Id = 1001, Name = "机械键盘", InternalCode = "SECRET-123", Price = 299.99m, CreatedAt = DateTime.Now }; // 方式1:最简单的序列化 string json = JsonSerializer.Serialize(product); Console.WriteLine(json); // 方式2:带配置的序列化(推荐) var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true, Encoder = System.Text. Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping // 中文不转义 }; string formattedJson = JsonSerializer. Serialize(product, options); Console.WriteLine(formattedJson); // 反序列化 string jsonData = """ { "id": 1001, "product_name": "机械键盘", "price": 299.99, "createdAt": "2026-01-22T10:30:00" } """; var deserializedProduct = JsonSerializer.Deserialize<Product>(jsonData, options); Console.WriteLine($"产品名称:{deserializedProduct.Name}");

image.png

踩坑预警:

  • 默认情况下中文会被编码成\uXXXX格式,记得设置JavaScriptEncoder.UnsafeRelaxedJsonEscaping
  • DateTime默认序列化为ISO 8601格式,如果需要自定义格式得写转换器(下个方案会讲)
  • 属性必须有public的getter/setter,只读属性默认不会反序列化

性能数据: 在测试中(序列化1万个Product对象),这种基础用法耗时约45ms,内存分配1.2MB


方案二:进阶玩法 - 自定义转换器

适用场景: 特殊格式处理、遗留系统对接、复杂业务逻辑

这是个分水岭,很多开发者卡在这里。我当时第一次写转换器也是查了半天文档,后来总结了个万能模板。

实战案例: 处理前端传来的多种日期格式

csharp
using System.Text.Json; using System.Text.Json.Serialization; // 自定义日期转换器 public class FlexibleDateTimeConverter : JsonConverter<DateTime> { private readonly string[] _formats = new[] { "yyyy-MM-dd HH:mm: ss", "yyyy/MM/dd HH:mm: ss", "yyyy-MM-dd", "yyyyMMdd" }; public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { string dateString = reader.GetString(); // 尝试多种格式解析 foreach (var format in _formats) { if (DateTime.TryParseExact(dateString, format, null, System.Globalization.DateTimeStyles.None, out DateTime result)) { return result; } } // 兜底方案 return DateTime.Parse(dateString); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { // 统一输出格式 writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss")); } } // 自定义枚举转换器(转成字符串而非数字) public class OrderStatus { public enum Status { Pending = 0, Processing = 1, Shipped = 2, Completed = 3 } } // 使用方式 public class Order { public int OrderId { get; set; } // 方式1:在属性上标注 [JsonConverter(typeof(FlexibleDateTimeConverter))] public DateTime OrderDate { get; set; } // 方式2:枚举转字符串 [JsonConverter(typeof(JsonStringEnumConverter))] public OrderStatus. Status Status { get; set; } public decimal TotalAmount { get; set; } } // 测试代码 var order = new Order { OrderId = 10001, OrderDate = DateTime.Now, Status = OrderStatus.Status.Processing, TotalAmount = 1888.88m }; var options = new JsonSerializerOptions { WriteIndented = true, Converters = { new FlexibleDateTimeConverter() } // 全局注册 }; string json = JsonSerializer.Serialize(order, options); Console.WriteLine(json); // 反序列化测试(支持多种日期格式) string testJson = """ { "OrderId": 10002, "OrderDate": "2026/01/22 14:30:00", "Status": "Shipped", "TotalAmount": 2999.00 } """; var parsedOrder = JsonSerializer.Deserialize<Order>(testJson, options); Console.WriteLine($"订单状态:{parsedOrder.Status}");

image.png

真实应用场景: 我们有个对接老系统的项目,对方日期格式五花八门,有时是yyyyMMdd,有时是yyyy/MM/dd,用这个转换器完美解决。

性能影响: 自定义转换器会比默认慢约15-20%,但在可接受范围。测试显示序列化1万个Order对象从45ms增加到54ms。

扩展建议:

  • 转换器可以处理更复杂逻辑,比如加密/解密、数据脱敏
  • 可以写个基类JsonConverter<T>封装通用逻辑
  • 记得做好异常处理,避免脏数据导致整个反序列化失败

方案三:性能极致 - Source Generator

适用场景: 高并发API、性能敏感服务、云函数/Serverless

这是我个人最推荐的方式,特别是. NET 6+项目。虽然代码稍微多一点,但性能提升是肉眼可见的。

csharp
using System.Text.Json; using System.Text.Json.Serialization; // 1. 定义序列化上下文(这是关键) [JsonSerializable(typeof(ApiResponse))] [JsonSerializable(typeof(List<Product>))] [JsonSourceGenerationOptions( PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false // 生产环境关闭美化 )] public partial class AppJsonContext : JsonSerializerContext { } // 2. 数据模型 public class ApiResponse { public int Code { get; set; } public string Message { get; set; } public List<Product> Data { get; set; } } // 3. 使用方式 public class PerformanceDemo { public static void Main() { var response = new ApiResponse { Code = 200, Message = "success", Data = new List<Product> { new Product { Id = 1, Name = "商品A", Price = 99.9m }, new Product { Id = 2, Name = "商品B", Price = 199.9m } } }; // 🚀 使用Source Generator序列化(推荐) string json = JsonSerializer.Serialize(response, AppJsonContext.Default. ApiResponse); Console.WriteLine(json); // 🚀 反序列化 var parsed = JsonSerializer. Deserialize(json, AppJsonContext.Default. ApiResponse); Console.WriteLine($"返回码:{parsed.Code}"); // 性能对比测试 PerformanceBenchmark(); } static void PerformanceBenchmark() { var testData = new ApiResponse { Code = 200, Message = "test", Data = Enumerable.Range(1, 1000).Select(i => new Product { Id = i, Name = $"商品{i}", Price = i * 10m, CreatedAt = DateTime. Now }).ToList() }; var sw = System.Diagnostics.Stopwatch.StartNew(); // 测试1:反射模式 for (int i = 0; i < 1000; i++) { _ = JsonSerializer.Serialize(testData); } sw.Stop(); Console.WriteLine($"反射模式:{sw.ElapsedMilliseconds}ms"); // 测试2:Source Generator sw.Restart(); for (int i = 0; i < 1000; i++) { _ = JsonSerializer.Serialize(testData, AppJsonContext.Default.ApiResponse); } sw. Stop(); Console.WriteLine($"Source Generator:{sw.ElapsedMilliseconds}ms"); } }

image.png

踩坑预警:

  1. 必须是partial class,不然编译器生成不了代码
  2. 所有要序列化的类型都得注册到Context里,漏了会运行时报错
  3. 泛型支持有限,复杂泛型可能得手动写辅助方法

进阶技巧: 多个Context隔离

csharp
// 按模块拆分Context,避免一个Context太臃肿 [JsonSerializable(typeof(UserInfo))] [JsonSerializable(typeof(LoginRequest))] public partial class AuthJsonContext : JsonSerializerContext { } [JsonSerializable(typeof(Order))] [JsonSerializable(typeof(OrderDetail))] public partial class OrderJsonContext : JsonSerializerContext { }

方案四:实战场景 - 流式处理大文件

适用场景: 日志处理、数据导出、大JSON文件解析

有次我处理一个500MB的JSON日志文件,用常规方式直接内存爆了。后来发现Text.Json有个流式API,完美解决。

csharp
using System.Text.Json; public class LogEntry { public DateTime Timestamp { get; set; } public string Level { get; set; } public string Message { get; set; } } public class StreamingJsonDemo { // 场景1:流式写入大量数据 public static async Task WriteLogsToFile(string filePath, IEnumerable<LogEntry> logs) { using var fileStream = File.Create(filePath); using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = false // 生产环境关闭美化节省空间 }); writer.WriteStartArray(); foreach (var log in logs) { JsonSerializer.Serialize(writer, log); // 每1000条刷新一次,避免占用太多内存 if (logs.ToList().IndexOf(log) % 1000 == 0) { await writer.FlushAsync(); } } writer.WriteEndArray(); } // 场景2:流式读取大文件 public static async Task<List<LogEntry>> ReadLogsFromFile(string filePath) { var logs = new List<LogEntry>(); using var fileStream = File.OpenRead(filePath); using var jsonDoc = await JsonDocument.ParseAsync(fileStream); foreach (var element in jsonDoc.RootElement. EnumerateArray()) { var log = new LogEntry { Timestamp = element.GetProperty("Timestamp").GetDateTime(), Level = element.GetProperty("Level").GetString(), Message = element.GetProperty("Message").GetString() }; logs.Add(log); } return logs; } // 场景3:逐行解析(最省内存) public static async IAsyncEnumerable<LogEntry> StreamReadLogs(string filePath) { using var fileStream = File.OpenRead(filePath); using var reader = new StreamReader(fileStream); await reader.ReadLineAsync(); // 跳过 '[' string line; while ((line = await reader.ReadLineAsync()) != null) { if (line. Trim() == "]") break; var cleanLine = line.TrimEnd(','); var log = JsonSerializer.Deserialize<LogEntry>(cleanLine); if (log != null) { yield return log; } } } // 性能测试 public static async Task TestPerformance() { var testLogs = Enumerable.Range(1, 100000).Select(i => new LogEntry { Timestamp = DateTime.Now, Level = "INFO", Message = $"Test log message {i}" }).ToList(); var filePath = "test_logs.json"; // 写入测试 var sw = System.Diagnostics.Stopwatch.StartNew(); await WriteLogsToFile(filePath, testLogs); sw.Stop(); Console.WriteLine($"写入10万条日志耗时:{sw.ElapsedMilliseconds}ms"); Console.WriteLine($"文件大小:{new FileInfo(filePath).Length / 1024 / 1024}MB"); // 流式读取测试 sw.Restart(); int count = 0; await foreach (var log in StreamReadLogs(filePath)) { count++; } sw.Stop(); Console.WriteLine($"流式读取{count}条日志耗时:{sw.ElapsedMilliseconds}ms"); Console.WriteLine($"峰值内存占用:约{GC.GetTotalMemory(false) / 1024 / 1024}MB"); } }

实测效果(处理10万条日志):

  • 传统方式:内存占用380MB,耗时1.8秒
  • 流式处理:内存占用35MB,耗时1.2秒
  • 内存节省90%以上!

适用场景清单:

  • ✅ 导出Excel转JSON
  • ✅ 处理第三方API返回的超大响应
  • ✅ 实时日志分析系统
  • ✅ 数据ETL任务

🎓 三个关键设计原则

在项目中用了两年多Text.Json,我总结出这三条铁律:

1️⃣ 配置复用原则

别每次序列化都new一个JsonSerializerOptions,这玩意儿创建成本不低。我建议这样:

csharp
public static class JsonConfig { // 单例模式复用配置 public static readonly JsonSerializerOptions Default = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNameCaseInsensitive = true }; public static readonly JsonSerializerOptions PrettyPrint = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }; } // 使用 string json = JsonSerializer.Serialize(data, JsonConfig.Default);

2️⃣ 异常边界原则

JSON反序列化是个高危操作,外部数据不可信。我习惯这样包一层:

csharp
public static class SafeJsonHelper { public static T TryDeserialize<T>(string json, T defaultValue = default) { try { return JsonSerializer.Deserialize<T>(json, JsonConfig.Default) ?? defaultValue; } catch (JsonException ex) { // 记录日志 Console.WriteLine($"JSON解析失败:{ex.Message}"); return defaultValue; } } }

3️⃣ 性能监控原则

别盲目优化,先测量。我常用这个简单的性能监控装饰器:

csharp
public class JsonPerformanceMonitor { public static string SerializeWithMetrics<T>(T value, JsonSerializerOptions options = null) { var sw = System.Diagnostics. Stopwatch.StartNew(); long memBefore = GC.GetTotalMemory(false); string result = JsonSerializer.Serialize(value, options); sw.Stop(); long memAfter = GC.GetTotalMemory(false); Console.WriteLine($"序列化耗时:{sw.ElapsedMilliseconds}ms,内存分配:{(memAfter - memBefore) / 1024}KB"); return result; } }

🎯 三句话总结

  1. 性能优先用Source Generator,开发效率优先用反射模式,两者可以共存
  2. 配置复用+异常处理+性能监控,这三件套能避免90%的坑
  3. 渐进式迁移不可怕,先从日志、缓存等模块开始,积累经验再扩大范围

💬 互动话题

话题1: 你在项目中遇到过哪些JSON序列化的奇葩问题?比如时区、精度丢失、循环引用等,欢迎留言分享踩坑经历!

话题2: 如果让你设计一个通用的JSON工具类,你会包含哪些功能?我准备开源一个JsonHelper库,想听听大家的需求。

小挑战: 试试用Text.Json实现一个"部分更新"功能——只序列化值发生变化的属性。提示:可以结合JsonDocument做差异对比。

🏷️ 相关标签

#CSharp #性能优化 #JSON序列化 #dotNET #后端开发 #系统架构

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!