C# JSON处理全解析:System.Text.Json与Newtonsoft.Json实战对比 1. 项目概述为什么C#开发者绕不开JSON处理如果你用C#做过任何形式的网络通信、配置文件读写或者数据持久化那你肯定和JSON打过交道。这玩意儿现在几乎是数据交换的“普通话”从Web API的请求响应体到应用程序的配置文件再到NoSQL数据库的文档存储无处不在。我干了十多年开发从早期的XML、二进制序列化一路走过来可以很负责任地说在C#里把JSON玩明白是提升开发效率和代码质量的基本功。这个“项目”标题——“C# json解析与生成json字符串”——听起来像是一个基础操作但背后涉及的选择、陷阱和性能考量足够写好几篇文章。今天我就以一个老码农的视角掰开揉碎了讲讲在C#里处理JSON的方方面面。我们不止要会调用JsonSerializer.Serialize和Deserialize更要搞清楚什么时候该用System.Text.Json什么时候又得请回Newtonsoft.Json怎么处理循环引用和自定义序列化面对超大JSON文件或者高并发场景又有哪些优化技巧。无论你是刚入门的新手还是想深化理解的老手这篇长文都能给你带来实实在在的干货。2. 核心工具选型System.Text.Json vs Newtonsoft.Json十年前在C#里处理JSONNewtonsoft.Json也就是大家常说的Json.NET是唯一且最佳的选择。它功能强大、生态成熟几乎成了.NET项目的标配。但自从.NET Core 3.0开始微软官方推出了System.Text.Json局面就变了。现在做新项目很多开发者都会面临这个“幸福的烦恼”到底该选哪个2.1 性能与标准的权衡System.Text.Json最大的卖点是性能。它是为高性能场景从头设计的底层利用了SpanT等现代.NET特性在序列化和反序列化速度上尤其是处理大量小对象时通常比Newtonsoft.Json快不少并且内存分配也更少。对于Web API服务器这种对吞吐量和延迟敏感的场景这点性能优势积累起来就很可观。另一方面Newtonsoft.Json的优势在于极高的灵活性和广泛的兼容性。它经历了十多年的实战检验能处理各种“非标准”但实际中又很常见的JSON场景。比如它默认支持循环引用对象A引用BB又引用A而System.Text.Json出于安全性和性能考虑默认是抛异常的。再比如它对日期格式、数字解析等有更宽松和可配置的策略。注意如果你的项目是全新的且性能是首要考虑特别是面向.NET 5/6/7那么优先考虑System.Text.Json。如果你的项目是遗留系统或者需要与大量使用Newtonsoft.Json特性的第三方库交互或者你非常依赖某些高级特性如高度自定义的合约解析那么继续使用Newtonsoft.Json可能是更稳妥的选择。2.2 基础用法对比为了让你有个直观感受我们来看一个最简单的例子。假设我们有一个Person类public class Person { public string Name { get; set; } public int Age { get; set; } public DateTime Birthday { get; set; } }使用 System.Text.Jsonusing System.Text.Json; var person new Person { Name 张三, Age 30, Birthday new DateTime(1993, 5, 20) }; // 序列化 string jsonString JsonSerializer.Serialize(person); // 输出: {Name:张三,Age:30,Birthday:1993-05-20T00:00:00} // 反序列化 var deserializedPerson JsonSerializer.DeserializePerson(jsonString);使用 Newtonsoft.Jsonusing Newtonsoft.Json; var person new Person { Name 张三, Age 30, Birthday new DateTime(1993, 5, 20) }; // 序列化 string jsonString JsonSerializer.Serialize(person); // 输出: {Name:张三,Age:30,Birthday:1993-05-20T00:00:00} // 反序列化 var deserializedPerson JsonSerializer.DeserializePerson(jsonString);乍一看基础API几乎一模一样这也是微软为了降低迁移成本有意设计的。但深入到配置项差异就出来了。2.3 关键配置差异解析日期格式处理Newtonsoft.Json默认使用ISO 8601格式但你可以通过DateFormatString轻松自定义。System.Text.Json在.NET 5及以前默认序列化DateTime和DateTimeOffset时遵循ISO 8601扩展格式如1993-05-20T00:00:00反序列化时也主要支持此格式。自定义格式需要编写自定义转换器JsonConverter稍显复杂。在.NET 6中它提供了更多内置的日期格式选项。属性名策略命名策略两者都支持。System.Text.Json通过JsonNamingPolicy来设置例如JsonNamingPolicy.CamelCase驼峰命名。Newtonsoft.Json通过ContractResolver设置例如new CamelCasePropertyNamesContractResolver()。实操心得在Web API开发中为了与前端JavaScript通常使用驼峰命名保持风格一致设置全局驼峰命名策略非常常见。在System.Text.Json中你可以在JsonSerializerOptions里统一设置。忽略空值Newtonsoft.JsonNullValueHandling NullValueHandling.IgnoreSystem.Text.JsonDefaultIgnoreCondition JsonIgnoreCondition.WhenWritingNull这个功能在减少网络传输数据量时很有用。下面是一个配置较为完整的System.Text.Json示例var options new JsonSerializerOptions { // 属性名使用驼峰命名法 PropertyNamingPolicy JsonNamingPolicy.CamelCase, // 写入JSON时忽略为null的属性 DefaultIgnoreCondition JsonIgnoreCondition.WhenWritingNull, // 美化输出缩进仅用于调试生产环境通常不用 WriteIndented true, // 允许但不推荐JSON注释 ReadCommentHandling JsonCommentHandling.Skip, // 允许数字以字符串形式读取应对某些前端不规范传参 NumberHandling JsonNumberHandling.AllowReadingFromString }; string json JsonSerializer.Serialize(person, options);3. 进阶序列化与反序列化技巧掌握了基础我们来看看实际项目中那些让人头疼的“坑”和高级用法。3.1 处理复杂对象与循环引用循环引用是对象关系映射ORM如Entity Framework Core中常见的问题。例如Order对象包含一个Customer而Customer又有一个Orders列表。public class Order { public int Id { get; set; } public Customer Customer { get; set; } } public class Customer { public int Id { get; set; } public ListOrder Orders { get; set; } new(); }如果你直接用System.Text.Json序列化一个Customer会收到JsonExceptionA possible object cycle was detected。这是为了防止无限递归和栈溢出。解决方案使用[JsonIgnore]特性在不需要序列化的属性上标记比如在Customer的Orders属性上标记[JsonIgnore]。这是最简单直接的方法但丢失了部分数据。使用ReferenceHandler.Preserve.NET 6这是System.Text.Json提供的解决方案。它会在JSON中为对象添加$id和$ref元数据来维护引用关系。var options new JsonSerializerOptions { ReferenceHandler ReferenceHandler.Preserve, WriteIndented true }; string json JsonSerializer.Serialize(customer, options);生成的JSON会包含$id: 1和$ref: 1这样的字段。注意这种格式不是标准JSON可能与其他语言或工具不兼容。使用Newtonsoft.Json设置PreserveReferencesHandling PreserveReferencesHandling.Objects它处理这类问题更成熟兼容性也更好。3.2 多态序列化处理继承和接口假设你有一个图形基类Shape和两个派生类Circle和Rectangle。你有一个ListShape里面混合了圆和矩形。如何正确序列化和反序列化System.Text.Json在.NET 7之前对多态支持较弱需要编写自定义转换器。从.NET 7开始它引入了原生的多态序列化支持通过[JsonDerivedType]特性。[JsonDerivedType(typeof(Circle), typeDiscriminator: circle)] [JsonDerivedType(typeof(Rectangle), typeDiscriminator: rectangle)] public class Shape { public string Color { get; set; } } public class Circle : Shape { public double Radius { get; set; } } public class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } } // 序列化 ListShape shapes new() { new Circle { Radius 5 }, new Rectangle { Width 10, Height 20 } }; var options new JsonSerializerOptions { WriteIndented true }; string json JsonSerializer.Serialize(shapes, options); // JSON中会包含一个 “$type”: “circle” 的鉴别器字段 // 反序列化 var deserializedShapes JsonSerializer.DeserializeListShape(json, options);对于更复杂的场景或.NET 7以下版本自定义JsonConverter是必经之路。而Newtonsoft.Json通过TypeNameHandling设置可以很方便地处理但出于安全考虑可能引发反序列化漏洞通常不建议自动包含类型信息而是手动指定。3.3 自定义转换器JsonConverter实战当内置的序列化规则不满足需求时就需要祭出大杀器——自定义转换器。常见场景包括将枚举序列化为字符串而非数字。自定义日期格式如yyyy-MM-dd。序列化Dictionary时使用非字符串键如Dictionaryint, string默认键会被转为字符串你可能希望保持为数字。处理特殊的第三方类型。这里以一个常见的“枚举转字符串”为例public enum UserRole { Admin, Editor, Viewer } public class User { public string Name { get; set; } public UserRole Role { get; set; } }默认序列化Role会输出数字0,1,2。我们希望输出AdminEditor。为枚举类型创建自定义转换器public class JsonStringEnumConverterT : JsonConverterT where T : struct, Enum { public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // 从JSON字符串读取并解析为枚举值 string enumString reader.GetString(); if (Enum.TryParseT(enumString, ignoreCase: true, out T value)) { return value; } // 如果无法解析可以返回默认值或抛出异常 return default; } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { // 将枚举值写入为字符串 writer.WriteStringValue(value.ToString()); } }使用这个转换器有两种方式1. 通过[JsonConverter]特性标记在属性或类上2. 将其添加到JsonSerializerOptions的Converters集合中。// 方式一标记在枚举类型上全局影响 [JsonConverter(typeof(JsonStringEnumConverterUserRole))] public enum UserRole { ... } // 方式二添加到Options中 var options new JsonSerializerOptions(); options.Converters.Add(new JsonStringEnumConverterUserRole()); var user new User { Name 李四, Role UserRole.Admin }; string json JsonSerializer.Serialize(user, options); // 输出{Name:李四,Role:Admin}实操心得编写自定义转换器时务必同时处理好Read和Write方法并考虑异常情况。对于复杂转换器单元测试是必不可少的。另外System.Text.Json的转换器是强类型的比Newtonsoft.Json的转换器在性能上更有优势。4. 高性能与流式处理当处理几MB、几十MB甚至更大的JSON文件时一次性将整个文件读入内存JsonSerializer.Deserializestring可能会导致内存压力过大甚至OutOfMemoryException。这时就需要流式处理。4.1 使用 Utf8JsonReader 和 Utf8JsonWriterSystem.Text.Json提供了这两个底层、高性能的类型用于向前只读解析和写入。场景你有一个巨大的JSON数组文件里面包含百万个用户对象你只需要找出其中名字为“张三”的用户。using FileStream fs File.OpenRead(huge_users.json); using Utf8JsonReader reader new Utf8JsonReader(fs.GetBytes()); // 实际应从流中异步读取Buffer while (reader.Read()) { if (reader.TokenType JsonTokenType.StartObject) { // 使用 JsonSerializer 反序列化当前对象这是一个高性能的 partial deserialization var user JsonSerializer.DeserializeUser(ref reader); if (user?.Name 张三) { Console.WriteLine($找到: {user.Id}); // 处理找到的对象然后可以break或继续 } } }这种方式允许你像游标一样在JSON中移动只在需要时反序列化特定对象极大地节省了内存。写入大量数据时使用Utf8JsonWriterusing var stream new MemoryStream(); using var writer new Utf8JsonWriter(stream, new JsonWriterOptions { Indented true }); writer.WriteStartArray(); // 开始写入数组 [ for (int i 0; i 100000; i) { writer.WriteStartObject(); writer.WriteNumber(id, i); writer.WriteString(name, $User_{i}); writer.WriteEndObject(); } writer.WriteEndArray(); // 结束数组 ] writer.Flush(); string json Encoding.UTF8.GetString(stream.ToArray());4.2 异步序列化与反序列化.NET 5 为System.Text.Json引入了原生的异步方法这对于处理基于流的I/O操作如网络、大文件非常重要可以避免阻塞线程。// 异步从网络流反序列化 using HttpClient client new HttpClient(); using Stream stream await client.GetStreamAsync(https://api.example.com/large-data); var data await JsonSerializer.DeserializeAsyncMyDataModel(stream); // 异步序列化到文件 var options new JsonSerializerOptions { WriteIndented true }; using FileStream createStream File.Create(output.json); await JsonSerializer.SerializeAsync(createStream, data, options); await createStream.DisposeAsync();性能关键点对于内存中的对象序列化成字符串同步方法JsonSerializer.Serialize()通常更快因为不涉及异步状态机的开销。异步的优势主要体现在I/O等待上。5. 实战问题排查与性能调优理论说再多不如踩几个坑来得实在。下面是我在项目中总结的一些常见问题和优化点。5.1 常见异常与解决方案异常信息可能原因解决方案JsonException: ‘$’ is invalid after a single JSON valueJSON格式不正确可能包含了多个根元素或尾部有多余字符。检查JSON来源是否纯净。可以使用在线JSON验证工具。对于读取文件确保编码正确UTF-8。JsonException: The JSON value could not be converted to System.Int32类型不匹配。例如JSON中是字符串123但C#属性是int。1. 确保数据源类型正确。2. 使用[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]特性允许从字符串读取数字.NET 6。3. 使用自定义转换器。JsonException: A possible object cycle was detected对象存在循环引用。参见3.1节使用[JsonIgnore]、ReferenceHandler.Preserve或调整对象模型。InvalidOperationException: Cannot get the value of a token type ‘Null’ as a string在Utf8JsonReader中尝试在TokenType不是String时调用GetString()。在调用GetString(),GetInt32()等之前先用TokenType属性判断当前令牌类型。反序列化后属性为null或默认值1. JSON中属性名与C#属性名不匹配大小写、命名策略。2. 属性没有公共setter。3. JSON中缺少该字段。1. 使用[JsonPropertyName(“jsonField”)]特性显式指定映射。2. 确保属性有public set;或使用支持非公共setter的选项JsonSerializerOptions.IncludeFields或 Newtonsoft的ContractResolver。3. 检查数据源。5.2 性能优化 checklist缓存JsonSerializerOptionsJsonSerializerOptions的创建和初始化成本较高。绝对不要在每次序列化/反序列化时都new一个。应该将其创建为单例或静态只读字段。// 错误做法 public string ToJson(Person p) JsonSerializer.Serialize(p, new JsonSerializerOptions { ... }); // 正确做法 private static readonly JsonSerializerOptions _options new() { PropertyNamingPolicy JsonNamingPolicy.CamelCase }; public string ToJson(Person p) JsonSerializer.Serialize(p, _options);使用源生成Source Generation .NET 6这是System.Text.Json性能优化的终极武器。它通过在编译时生成序列化代码完全避免了运行时的反射开销能显著提升首次调用速度和减少内存分配。[JsonSerializable(typeof(Person))] [JsonSerializable(typeof(ListPerson))] internal partial class MyJsonContext : JsonSerializerContext { } // 使用源生成的序列化器 string json JsonSerializer.Serialize(person, MyJsonContext.Default.Person); var person2 JsonSerializer.Deserialize(json, MyJsonContext.Default.Person);对于高性能ASP.NET Core Web API为你的请求/响应模型启用源生成是标准操作。选择合适的数据结构反序列化到JsonDocument或JsonNode.NET 6进行DOM操作比反序列化到强类型对象更快但使用起来不如对象方便。如果你只需要读取JSON中的一小部分数据JsonDocument是更好的选择。using JsonDocument doc JsonDocument.Parse(jsonString); string name doc.RootElement.GetProperty(user).GetProperty(name).GetString();在Newtonsoft.Json中启用DefaultContractResolver缓存对于Newtonsoft.Json同样要缓存JsonSerializerSettings或ContractResolver实例。5.3 与Web APIASP.NET Core的集成在ASP.NET Core中JSON序列化是MVC框架的核心部分。在Program.cs或Startup.cs中你可以配置全局的JSON选项。对于 System.Text.Jsonbuilder.Services.ConfigureJsonOptions(options { options.SerializerOptions.PropertyNamingPolicy JsonNamingPolicy.CamelCase; options.SerializerOptions.DefaultIgnoreCondition JsonIgnoreCondition.WhenWritingNull; // 添加自定义转换器 options.SerializerOptions.Converters.Add(new MyCustomConverter()); });如果你想在项目中使用 Newtonsoft.Json比如因为引用的某些库依赖它需要安装Microsoft.AspNetCore.Mvc.NewtonsoftJson包然后builder.Services.AddControllers().AddNewtonsoftJson(options { options.SerializerSettings.ContractResolver new CamelCasePropertyNamesContractResolver(); options.SerializerSettings.NullValueHandling NullValueHandling.Ignore; });一个重要的实践对于API接口明确你的输入输出格式。使用[FromBody]特性时确保客户端发送的Content-Type是application/json。对于日期最好在前后端约定统一的格式如ISO 8601并在序列化设置中固定下来避免歧义。6. 总结与个人工具箱分享聊了这么多最后分享几点我个人的体会和工具箱里的小技巧。首先关于选型我现在的新项目默认首选System.Text.Json。它的性能优势在微服务和云原生架构下是实实在在的收益。除非项目有明确的、System.Text.Json目前无法很好满足的强需求比如对某些Newtonsoft.Json特有特性的重度依赖否则我不会轻易引入Newtonsoft.Json这个额外的依赖。第二一定要重视配置的复用。无论是JsonSerializerOptions还是JsonSerializerSettings把它们定义在容易管理的地方比如一个静态工具类或者通过依赖注入配置。乱new对象不仅影响性能还可能导致序列化行为不一致埋下诡异的bug。第三善用源生成。如果你的项目是.NET 6并且有性能要求花点时间配置源生成是值得的。它带来的启动性能提升和运行时内存减少在容器化部署和Serverless场景下尤其有用。第四处理“脏数据”要小心。现实世界中的JSON数据很少是完美的可能有多余的逗号、注释、数字写成字符串、日期格式千奇百怪。System.Text.Json默认比较严格这是好事有助于数据质量但也需要你根据实际情况调整JsonReaderOptions如AllowTrailingCommas、CommentHandling或使用自定义转换器来增加鲁棒性。最后调试小技巧当序列化结果不符合预期时别急着写代码。先把你内存中的对象用默认设置序列化出来看看JSON长什么样。再用你期望的JSON格式尝试用JsonSerializer.Deserialize反序列化看会不会报错。很多时候问题就出在属性名大小写、只读属性或者嵌套对象的初始化上。对于复杂的JSON结构使用像JsonDocument或在线格式化工具先进行可视化分析能帮你快速定位问题节点。JSON处理就像C#开发中的水和电基础但至关重要。把它理解透彻配置得当能让你在数据处理上省下大量时间和精力避免很多低级错误。希望这篇长文能成为你手边一份有用的参考。