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#里处理JSON,Newtonsoft.Json(也就是大家常说的Json.NET)是唯一且最佳的选择。它功能强大、生态成熟,几乎成了.NET项目的标配。但自从.NET Core 3.0开始,微软官方推出了System.Text.Json,局面就变了。现在做新项目,很多开发者都会面临这个“幸福的烦恼”:到底该选哪个?
2.1 性能与标准的权衡
System.Text.Json最大的卖点是性能。它是为高性能场景从头设计的,底层利用了Span<T>等现代.NET特性,在序列化和反序列化速度上,尤其是处理大量小对象时,通常比Newtonsoft.Json快不少,并且内存分配也更少。对于Web API服务器这种对吞吐量和延迟敏感的场景,这点性能优势积累起来就很可观。
另一方面,Newtonsoft.Json的优势在于极高的灵活性和广泛的兼容性。它经历了十多年的实战检验,能处理各种“非标准”但实际中又很常见的JSON场景。比如,它默认支持循环引用(对象A引用B,B又引用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.Json:
using 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.Deserialize<Person>(jsonString);使用 Newtonsoft.Json:
using 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.Deserialize<Person>(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.Json:NullValueHandling = NullValueHandling.IgnoreSystem.Text.Json:DefaultIgnoreCondition = 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 List<Order> Orders { get; set; } = new(); }如果你直接用System.Text.Json序列化一个Customer,会收到JsonException:A possible object cycle was detected。这是为了防止无限递归和栈溢出。
解决方案:
- 使用
[JsonIgnore]特性:在不需要序列化的属性上标记,比如在Customer的Orders属性上标记[JsonIgnore]。这是最简单直接的方法,但丢失了部分数据。 - 使用
ReferenceHandler.Preserve(.NET 6+):这是System.Text.Json提供的解决方案。它会在JSON中为对象添加$id和$ref元数据来维护引用关系。
生成的JSON会包含var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, WriteIndented = true }; string json = JsonSerializer.Serialize(customer, options);"$id": "1"和"$ref": "1"这样的字段。注意:这种格式不是标准JSON,可能与其他语言或工具不兼容。 - 使用
Newtonsoft.Json:设置PreserveReferencesHandling = PreserveReferencesHandling.Objects,它处理这类问题更成熟,兼容性也更好。
3.2 多态序列化(处理继承和接口)
假设你有一个图形基类Shape和两个派生类Circle和Rectangle。你有一个List<Shape>,里面混合了圆和矩形。如何正确序列化和反序列化?
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; } } // 序列化 List<Shape> 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.Deserialize<List<Shape>>(json, options);对于更复杂的场景或.NET 7以下版本,自定义JsonConverter是必经之路。而Newtonsoft.Json通过TypeNameHandling设置可以很方便地处理,但出于安全考虑(可能引发反序列化漏洞),通常不建议自动包含类型信息,而是手动指定。
3.3 自定义转换器(JsonConverter)实战
当内置的序列化规则不满足需求时,就需要祭出大杀器——自定义转换器。常见场景包括:
- 将枚举序列化为字符串(而非数字)。
- 自定义日期格式(如
yyyy-MM-dd)。 - 序列化
Dictionary时使用非字符串键(如Dictionary<int, string>,默认键会被转为字符串,你可能希望保持为数字)。 - 处理特殊的第三方类型。
这里以一个常见的“枚举转字符串”为例:
public enum UserRole { Admin, Editor, Viewer } public class User { public string Name { get; set; } public UserRole Role { get; set; } }默认序列化Role会输出数字(0,1,2)。我们希望输出"Admin","Editor"。
为枚举类型创建自定义转换器:
public class JsonStringEnumConverter<T> : JsonConverter<T> where T : struct, Enum { public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // 从JSON字符串读取,并解析为枚举值 string enumString = reader.GetString(); if (Enum.TryParse<T>(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(JsonStringEnumConverter<UserRole>))] public enum UserRole { ... } // 方式二:添加到Options中 var options = new JsonSerializerOptions(); options.Converters.Add(new JsonStringEnumConverter<UserRole>()); 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.Deserialize<string>)可能会导致内存压力过大甚至OutOfMemoryException。这时就需要流式处理。
4.1 使用 Utf8JsonReader 和 Utf8JsonWriter
System.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.Deserialize<User>(ref reader); if (user?.Name == "张三") { Console.WriteLine($"找到: {user.Id}"); // 处理找到的对象,然后可以break或继续 } } }这种方式允许你像游标一样在JSON中移动,只在需要时反序列化特定对象,极大地节省了内存。
写入大量数据时,使用Utf8JsonWriter:
using 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.DeserializeAsync<MyDataModel>(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 value | JSON格式不正确,可能包含了多个根元素或尾部有多余字符。 | 检查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
缓存
JsonSerializerOptions:JsonSerializerOptions的创建和初始化成本较高。绝对不要在每次序列化/反序列化时都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(List<Person>))] 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 API(ASP.NET Core)的集成
在ASP.NET Core中,JSON序列化是MVC框架的核心部分。在Program.cs或Startup.cs中,你可以配置全局的JSON选项。
对于 System.Text.Json:
builder.Services.Configure<JsonOptions>(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#开发中的水和电,基础但至关重要。把它理解透彻,配置得当,能让你在数据处理上省下大量时间和精力,避免很多低级错误。希望这篇长文能成为你手边一份有用的参考。
