C# JSON解析全攻略:从Newtonsoft.Json到System.Text.Json性能实战

📅 2026/8/7 5:33:29
C# JSON解析全攻略:从Newtonsoft.Json到System.Text.Json性能实战
1. 项目概述为什么C#开发者必须精通JSON解析在今天的软件开发里JSONJavaScript Object Notation几乎成了数据交换的“普通话”。无论是调用一个Web API、读取配置文件还是在不同服务间传递消息你大概率会碰到它。作为一名C#开发者如果你还在用字符串拼接和截取的方式来处理JSON那就像是在高速公路上骑自行车——不是不行但效率低且危险。我见过不少项目因为JSON解析不当导致数据错乱、性能瓶颈甚至安全漏洞。所以花时间把JSON解析吃透绝对不是浪费时间而是提升你开发效率和代码质量的必经之路。C#社区为处理JSON提供了非常强大的工具链从经典的Newtonsoft.Json到如今官方力推的System.Text.Json选择很多但坑也不少。这篇文章我会结合我十多年的C#开发经验从最基础的序列化反序列化到高性能场景下的流式处理、自定义转换器再到那些官方文档里不会写的“坑”和实战技巧为你彻底拆解C#解析JSON的方方面面。无论你是刚接触C#的新手还是想优化现有代码的老鸟都能在这里找到直接能用的“干货”。2. 核心工具选型Newtonsoft.Json 与 System.Text.Json 的深度对比当你准备在C#项目中处理JSON时第一个灵魂拷问就是用哪个库这直接决定了你后续开发的体验和代码的性能。目前主流的选择有两个老牌劲旅Newtonsoft.Json又称Json.NET和微软官方的后起之秀System.Text.Json。很多新手会直接问“哪个更好”但我的经验是没有绝对的好坏只有是否适合你的场景。2.1 Newtonsoft.Json功能全面的“瑞士军刀”Newtonsoft.Json由 James Newton-King 创建在过去十多年里一直是C#社区处理JSON的事实标准。它的最大优点是功能极其丰富几乎你能想到的所有JSON操作场景它都有对应的解决方案。它的核心优势在于灵活性和容错性高度可配置的序列化行为你可以通过JsonSerializerSettings精细控制几乎所有细节比如如何处理空值、日期格式、循环引用、命名策略驼峰、帕斯卡等。例如你想让所有属性名在序列化时自动转为小写蛇形命名snake_case几行配置就能搞定。强大的LINQ to JSONJToken体系除了简单的对象映射它还提供了JObjectJArrayJToken等动态类型。当你需要在不定义强类型类的情况下动态查询、修改或创建JSON结构时这个功能是无价之宝。比如解析一个结构不确定的第三方API响应提取其中某个深层嵌套的字段。出色的容错能力对于格式不太严格或包含额外字段的JSONNewtonsoft.Json通常能更“宽容”地处理而不会直接抛出异常。一个典型的Newtonsoft.Json反序列化示例using Newtonsoft.Json; public class Person { public string Name { get; set; } public int Age { get; set; } public DateTime Birthday { get; set; } } string json {name: 张三, age: 30, birthday: 1993-05-15}; // 配置序列化设置日期格式和命名策略 var settings new JsonSerializerSettings { DateFormatString yyyy-MM-dd, // ContractResolver 等可以用于更复杂的命名映射 }; Person person JsonConvert.DeserializeObjectPerson(json, settings); Console.WriteLine(person.Name); // 输出张三注意Newtonsoft.Json默认使用反射来获取和设置属性这在大量、高频操作的场景下可能会成为性能瓶颈。虽然它功能强大但“重量”也相对较大。2.2 System.Text.Json追求性能的“官方新锐”从.NET Core 3.0开始微软推出了System.Text.Json作为内置库其设计初衷就是高性能和低内存分配。如果你的项目目标是高吞吐量、低延迟例如微服务、Web API后端那么它往往是更好的选择。它的核心优势在于性能和安全性原生高性能它利用SpanT和Utf8JsonReader/Writer等现代.NET特性在序列化和反序列化时速度更快内存开销更小。根据官方基准测试其性能通常数倍于Newtonsoft.Json。默认安全为了性能和安全性它做了一些“不那么宽容”的设计。例如属性名称默认区分大小写并且默认不允许尾随逗号。这迫使开发者写出更规范的JSON减少了潜在的错误。异步序列化支持原生支持SerializeAsync和DeserializeAsync对于处理大文件或网络流非常友好。源码生成器Source Generator这是它的“大杀器”。通过源码生成可以在编译时而非运行时生成序列化/反序列化代码彻底消除反射开销性能可以达到极致。对于性能敏感的热点路径这是必选项。一个典型的System.Text.Json反序列化示例using System.Text.Json; public class Person { // 使用特性进行精细控制 [JsonPropertyName(full_name)] // 映射JSON中的不同字段名 public string Name { get; set; } public int Age { get; set; } [JsonConverter(typeof(DateTimeConverter))] // 使用自定义转换器 public DateTime Birthday { get; set; } } string json {full_name: 李四, age: 25, birthday: 1998-08-20}; // 配置选项 var options new JsonSerializerOptions { PropertyNameCaseInsensitive true, // 启用不区分大小写 WriteIndented true // 美化输出格式化 }; Person person JsonSerializer.DeserializePerson(json, options); Console.WriteLine(person.Name); // 输出李四2.3 选型决策指南我该如何选择为了避免你纠结我根据自己的经验总结了一个决策表特性/场景Newtonsoft.JsonSystem.Text.Json我的建议新项目.NET 5-✅优先使用 System.Text.Json。它是框架的一部分无需额外依赖且为未来性能优化铺平道路。遗留项目或大量依赖✅-如果项目已深度集成 Newtonsoft.Json且无显著性能问题不建议盲目迁移成本可能很高。需要极致性能-✅尤其用源码生成器必须使用 System.Text.Json 源码生成器。对于高频调用的API、游戏、金融交易等场景这是唯一选择。处理不规则/动态JSON✅JObject好用⚠️可用JsonNode但稍弱如果需要频繁动态操作JSON结构Newtonsoft.Json 的 LINQ to JSON 更顺手。需要高度自定义序列化✅极其灵活✅足够但略繁琐两者都能满足绝大多数需求。Newtonsoft.Json 的配置方式有时更直观。目标框架是 .NET Framework✅⚠️需要额外安装老项目用 Newtonsoft.Json。.NET Framework 项目虽可安装 System.Text.Json 包但体验非原生。实操心得在实际项目中我经常采用“混合策略”。主体架构和新模块使用System.Text.Json以保证性能和现代性。但对于那些需要处理非常动态、结构不定的第三方数据接口的特定模块我会单独引入Newtonsoft.Json来处理避免把核心代码搞复杂。只需注意控制好两者的使用边界不要混用同一个对象的序列化/反序列化即可。3. 基础到精通System.Text.Json 核心操作全解析选定System.Text.Json作为主力后我们来深入它的核心用法。掌握这些你就能应对90%的日常开发场景。3.1 序列化与反序列化对象与JSON的互转这是最基础也是最常用的操作。JsonSerializer.Serialize和JsonSerializer.Deserialize是你的主要工具。基础用法var product new Product { Id 1, Name 笔记本电脑, Price 5999.99M, InStock true }; string jsonString JsonSerializer.Serialize(product); // 输出{Id:1,Name:笔记本电脑,Price:5999.99,InStock:true} Product deserializedProduct JsonSerializer.DeserializeProduct(jsonString);通过JsonSerializerOptions控制行为这个选项对象是你定制序列化过程的关键。常用的配置有var options new JsonSerializerOptions { WriteIndented true, // 美化输出带缩进和换行用于调试 PropertyNamingPolicy JsonNamingPolicy.CamelCase, // 属性名转为驼峰命名 DefaultIgnoreCondition JsonIgnoreCondition.WhenWritingNull, // 忽略null值 Encoder JavaScriptEncoder.UnsafeRelaxedJsonEscaping // 放宽字符转义如中文 }; string json JsonSerializer.Serialize(product, options);注意PropertyNamingPolicy只影响序列化输出时的属性名。反序列化时默认是区分大小写进行匹配的。如果需要反序列化时也忽略大小写必须显式设置PropertyNameCaseInsensitive true。这是新手常踩的坑。3.2 处理特殊数据类型日期、枚举、集合JSON标准本身不支持日期和枚举等类型所以如何处理它们需要额外配置。日期时间默认序列化为 ISO 8601 格式如2023-10-27T14:48:00Z。你可以通过JsonSerializerOptions.Converters添加自定义转换器或者使用内置的JsonConverter。options.Converters.Add(new JsonStringEnumConverter()); // 枚举序列化为字符串 // 自定义日期格式 options.Converters.Add(new DateTimeConverterUsingDateTimeParse()); // 需自己实现枚举默认序列化为数字。使用JsonStringEnumConverter可以将其序列化为字符串可读性更好也便于与其他系统交互。集合与字典ListT、Dictionarystring, T等都能被很好地支持。字典的Key如果是复杂类型需要特别注意。3.3 使用特性进行精细控制通过在模型类上添加特性Attribute你可以实现更精细的字段级控制而无需全局配置。public class Order { [JsonPropertyName(order_id)] // 序列化后字段名为 order_id public int Id { get; set; } [JsonIgnore] // 完全忽略此属性不参与序列化和反序列化 public string InternalCode { get; set; } [JsonInclude] // 即使是非公共属性如私有setter也包含在序列化中 public string CustomerNote { get; private set; } [JsonConverter(typeof(CustomDecimalConverter))] // 为该属性指定自定义转换器 public decimal TotalAmount { get; set; } [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] // 允许数字从字符串读取 public int Quantity { get; set; } }实操心得我倾向于将全局配置JsonSerializerOptions用于项目级的统一约定如驼峰命名、忽略Null值而将特性用于处理个别字段的特殊情况。这样代码的意图更清晰也避免了全局配置被过度修改导致难以维护。4. 高级应用与性能优化实战当你掌握了基础就可以挑战更复杂的场景和追求极致的性能了。这部分内容能让你从“会用”升级到“精通”。4.1 动态JSON处理JsonDocument 与 JsonNode不是所有JSON都能对应到预定义的C#类。对于需要查询、修改或动态构建的JSONJsonDocument和JsonNode是你的利器。JsonDocument提供只读、高性能的文档对象模型DOM。它使用Utf8JsonReader在后台解析内存效率极高适合仅需查询一次或几次的大型JSON。string json { name: 数据中心, servers: [ {id: 1, status: online}, {id: 2, status: offline} ] }; using JsonDocument document JsonDocument.Parse(json); JsonElement root document.RootElement; string name root.GetProperty(name).GetString(); foreach (JsonElement server in root.GetProperty(servers).EnumerateArray()) { int id server.GetProperty(id).GetInt32(); // ... 处理服务器信息 } // 使用 using 确保及时释放资源重要提示JsonDocument和从它获取的JsonElement是ref struct它们的数据直接指向原始JSON数据的缓冲区因此不能存储在类的字段中也不能在异步方法中跨越 await 使用。它的设计就是为了高性能的单次遍历。JsonNode.NET 6提供可变的DOM类似于Newtonsoft.Json的JToken。你可以方便地添加、修改和删除节点。JsonNode rootNode JsonNode.Parse(json)!; // 修改值 rootNode[name] 新数据中心; // 添加新节点 rootNode[location] 上海; // 获取新的JSON字符串 string newJson rootNode.ToJsonString();JsonNode用起来更灵活但性能开销比JsonDocument大。选择原则是只读查询用JsonDocument需要修改则用JsonNode。4.2 自定义JsonConverter处理“奇葩”数据格式当你遇到无法用内置规则序列化的类型时就需要自定义JsonConverterT。常见场景包括将特定的字符串格式如1,2,3,4反序列化为Listint。处理多态类型一个属性可能是A类也可能是B类。自定义日期格式如Unix时间戳。示例实现一个将Point结构体序列化为x,y字符串的转换器。public struct Point { public int X; public int Y; } public class PointConverter : JsonConverterPoint { public override Point Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // 预期JSON格式10,20 string pointString reader.GetString()!; var parts pointString.Split(,); return new Point { X int.Parse(parts[0]), Y int.Parse(parts[1]) }; } public override void Write(Utf8JsonWriter writer, Point value, JsonSerializerOptions options) { writer.WriteStringValue(${value.X},{value.Y}); } } // 使用 var options new JsonSerializerOptions(); options.Converters.Add(new PointConverter()); Point p new Point { X 10, Y 20 }; string json JsonSerializer.Serialize(p, options); // 输出10,20 Point p2 JsonSerializer.DeserializePoint(\5,15\, options); // 从字符串反序列化实操心得编写自定义转换器时务必处理好边界情况如空值、格式错误等。在Read方法中要充分利用Utf8JsonReader提供的方法TryGet...来安全地读取数据而不是直接Get...然后期待它一定正确。4.3 性能核武器源码生成器Source Generator这是System.Text.Json在性能上的终极解决方案。它通过在编译时分析你的类型直接生成高效的序列化代码完全避免了运行时的反射和动态代码生成。如何使用在项目中安装System.Text.Json包通常已包含。创建一个分部类partial class并为其添加[JsonSerializable]特性。在项目文件中启用源码生成。示例// 你的模型 public class HighPerfModel { public int Id { get; set; } public string Data { get; set; } } // 在另一个文件如 HighPerfModel.JsonContext.cs中定义上下文 [JsonSerializable(typeof(HighPerfModel))] [JsonSerializable(typeof(ListHighPerfModel))] public partial class AppJsonContext : JsonSerializerContext { }在项目文件 (.csproj) 中确保有PropertyGroup LangVersionlatest/LangVersion /PropertyGroup现在你可以使用生成的上下文进行序列化性能极高var model new HighPerfModel { Id 1, Data test }; // 使用生成的序列化器 byte[] jsonBytes JsonSerializer.SerializeToUtf8Bytes(model, AppJsonContext.Default.HighPerfModel); HighPerfModel deserialized JsonSerializer.Deserialize(jsonBytes, AppJsonContext.Default.HighPerfModel)!;性能对比实测在我参与的一个高频交易数据推送服务中将核心模型的序列化从反射模式切换到源码生成模式后JSON处理部分的CPU耗时下降了约65%GC压力也显著减少。对于热点路径这绝对是值得投入的优化。5. 实战避坑指南与疑难问题排查理论讲得再多不如踩几个坑来得实在。下面是我在多年开发中积累的一些常见问题和解决方案希望能帮你少走弯路。5.1 循环引用与对象引用处理当你序列化的对象图中存在循环引用例如Order对象包含Customer而Customer又有一个Orders列表指向回原来的Order默认的序列化器会陷入无限循环并最终抛出JsonException。在 System.Text.Json 中处理System.Text.Json默认不处理循环引用这是出于性能和语义清晰度的考虑。你有几种选择修改数据模型这是最推荐的方式。使用DTO数据传输对象在序列化时只包含必要的、非循环的数据。例如Customer的序列化视图里不包含Orders列表。使用[JsonIgnore]特性在会引起循环的属性上标记忽略。使用ReferenceHandler.Preserve.NET 6这会在JSON中插入$id和$ref元数据来保留引用信息。var options new JsonSerializerOptions { ReferenceHandler ReferenceHandler.Preserve, WriteIndented true };注意这样生成的JSON不是标准JSON可能与其他不支持此元数据的系统不兼容。与 Newtonsoft.Json 的差异Newtonsoft.Json可以通过ReferenceLoopHandling.Ignore简单地忽略循环引用或者用Preserve模式。在迁移旧代码时如果发现循环引用问题需要仔细审查数据模型。5.2 多态类型反序列化一个接口多种实现这是另一个经典难题。JSON中有一个Type字段根据它的值需要反序列化成不同的具体类。[ { Type: Circle, Radius: 5 }, { Type: Rectangle, Width: 10, Height: 20 } ]在 System.Text.Json 中的解决方案你需要编写一个自定义的JsonConverter。基本思路是在Write方法中写入一个类型鉴别器如Type字段在Read方法中读取这个鉴别器然后决定反序列化成哪种具体类型。这需要一些样板代码社区也有不少辅助库可以简化这个过程。一个简化的模式[JsonConverter(typeof(ShapeConverter))] public abstract class Shape { public abstract string Type { get; } } public class Circle : Shape { public override string Type Circle; public double Radius { get; set; } } public class ShapeConverter : JsonConverterShape { public override Shape Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using var doc JsonDocument.ParseValue(ref reader); var root doc.RootElement; var type root.GetProperty(Type).GetString(); return type switch { Circle root.DeserializeCircle(options), _ throw new JsonException($未知的形状类型: {type}) }; } // Write 方法也需要相应实现 }5.3 常见异常与调试技巧JsonException: The JSON value could not be converted to...原因最常见。JSON中的数据类型与C#属性类型不匹配如JSON是字符串但C#属性是int。排查仔细对比JSON字符串和你的模型类。使用JsonSerializerOptions.PropertyNameCaseInsensitive true可以解决大小写不匹配问题。使用[JsonNumberHandling]特性可以处理数字以字符串形式出现的情况。性能问题在循环中频繁创建JsonSerializerOptions。原因JsonSerializerOptions在首次使用时需要初始化创建成本较高。解决务必将其缓存起来作为静态单例使用。这是提升性能最简单有效的方法之一。public static class JsonDefaults { public static readonly JsonSerializerOptions DefaultOptions new JsonSerializerOptions { PropertyNameCaseInsensitive true, PropertyNamingPolicy JsonNamingPolicy.CamelCase // ... 其他配置 }; }内存泄漏与 JsonDocument 相关忘记释放JsonDocument。原因JsonDocument实现了IDisposable。如果解析大量JSON而不释放会导致内存堆积。解决始终使用using语句包裹JsonDocument的使用或者确保在不再需要时调用Dispose()。调试技巧当反序列化失败时异常信息可能不够详细。一个有用的方法是先尝试用JsonDocument.Parse或JsonNode.Parse解析JSON看看结构是否如你所想。或者在序列化时使用WriteIndented true输出格式化的JSON便于肉眼检查。6. 综合实战构建一个健壮的配置读取器让我们用一个完整的、贴近实际的例子来串联以上所有知识点编写一个从JSON文件读取应用配置的类。这个配置可能很复杂包含嵌套对象、数组、枚举并且我们需要它高性能、易扩展。目标读取如下appsettings.json{ AppName: 订单处理服务, LogLevel: Information, Database: { ConnectionString: Server.;DatabaseOrderDb;, TimeoutSeconds: 30 }, Endpoints: [ { Name: API, Url: https://api.example.com, Protocol: Https }, { Name: Webhook, Url: http://localhost:8080, Protocol: Http } ], Features: { EnableCache: true, CacheDurationMinutes: 5 } }步骤1定义强类型配置模型public class AppConfig { public string AppName { get; set; } string.Empty; public LogLevel LogLevel { get; set; } // 枚举类型 public DatabaseConfig Database { get; set; } new(); public ListEndpointConfig Endpoints { get; set; } new(); public FeatureFlags Features { get; set; } new(); } public class DatabaseConfig { public string ConnectionString { get; set; } string.Empty; public int TimeoutSeconds { get; set; } } public class EndpointConfig { public string Name { get; set; } string.Empty; public string Url { get; set; } string.Empty; public Protocol Protocol { get; set; } } public enum Protocol { Http, Https } public enum LogLevel { Debug, Information, Warning, Error } public class FeatureFlags { public bool EnableCache { get; set; } public int CacheDurationMinutes { get; set; } }步骤2实现高性能配置读取器我们使用源码生成器来获得最佳性能并异步读取文件。[JsonSerializable(typeof(AppConfig))] public partial class AppConfigJsonContext : JsonSerializerContext { } public static class ConfigurationLoader { // 缓存配置选项和上下文 private static readonly JsonSerializerOptions s_options new() { PropertyNameCaseInsensitive true, WriteIndented true // 仅调试用生产环境可关闭 }; public static async TaskAppConfig LoadConfigAsync(string filePath) { if (!File.Exists(filePath)) throw new FileNotFoundException($配置文件未找到: {filePath}); await using FileStream fileStream File.OpenRead(filePath); // 使用源码生成的反序列化方法性能最优 AppConfig? config await JsonSerializer.DeserializeAsync( fileStream, AppConfigJsonContext.Default.AppConfig // 使用生成的上下文 ); return config ?? throw new InvalidOperationException(反序列化配置失败结果为null。); } // 备用方法使用反射无需源码生成上下文 public static async TaskAppConfig LoadConfigReflectionAsync(string filePath) { await using FileStream fileStream File.OpenRead(filePath); AppConfig? config await JsonSerializer.DeserializeAsyncAppConfig(fileStream, s_options); return config ?? throw new InvalidOperationException(反序列化配置失败。); } }步骤3使用配置// 在Program.cs或启动类中 var config await ConfigurationLoader.LoadConfigAsync(appsettings.json); Console.WriteLine($应用名称: {config.AppName}); Console.WriteLine($数据库超时: {config.Database.TimeoutSeconds}秒); foreach (var endpoint in config.Endpoints) { Console.WriteLine($端点: {endpoint.Name} - {endpoint.Url}); }这个实战案例涵盖的关键点模型设计使用嵌套类清晰地对齐JSON结构。枚举处理默认序列化为字符串可读性好。异步操作使用DeserializeAsync处理文件流避免阻塞。性能优化使用源码生成器上下文 (AppConfigJsonContext) 进行反序列化这是生产环境的最佳实践。错误处理检查文件存在性处理反序列化结果为null的情况。配置复用将JsonSerializerOptions缓存为静态字段避免重复创建开销。通过这样一个完整的例子你应该能体会到将System.Text.Json的各项特性组合起来可以构建出既健壮又高性能的解决方案。关键在于根据场景选择正确的工具和方法而不是死记硬背API。JSON解析本身不复杂但把它用对、用好却是区分普通开发者和资深开发者的一个细节。