C# JSON处理全攻略:Newtonsoft.Json与System.Text.Json深度对比与实战

📅 2026/7/31 5:12:46
C# JSON处理全攻略:Newtonsoft.Json与System.Text.Json深度对比与实战
1. 项目概述从数据泥潭到清晰结构在C#开发里尤其是做Web API、桌面应用数据交换或者配置文件处理时JSON几乎是无处不在的。你可能经常遇到这样的场景从某个接口拿到一串长得让人眼花的文本里面层层嵌套着各种括号和引号你需要从中精准地提取出用户的名字或者订单的价格又或者你手头有一个Customer对象里面有几十个属性你需要把它打包成一个整洁的字符串发送给另一个系统。这个过程就是JSON的解析与生成。听起来简单不就是用个Newtonsoft.Json或者System.Text.Json吗但实际踩过坑的都知道日期格式怎么处理、循环引用怎么避免、超大JSON怎么高效读取、属性名怎么按驼峰命名输出每一个细节都可能让你调试半天。这篇文章我就结合自己这些年处理各种“奇葩”JSON格式的经验把C#里玩转JSON的那些核心技巧、隐藏陷阱和性能优化点给你掰开揉碎了讲清楚。无论你是刚接触C#的新手还是想优化现有代码的老手这里都有你能直接“抄作业”的解决方案。2. 核心工具选型Newtonsoft.Json 与 System.Text.Json 的深度对比十年前C#处理JSON几乎只有Newtonsoft.Json也就是Json.NET这一个选择它强大、灵活社区生态极好。但后来微软官方推出了System.Text.Json旨在提供更高性能、更安全的默认方案。现在问题来了我们该怎么选这不是一个非此即彼的问题而是一个基于场景的权衡。2.1 性能与内存效率System.Text.Json 的压倒性优势如果你的应用对性能极其敏感比如高频微服务接口、实时数据处理流水线那么System.Text.Json通常是首选。它的底层采用了SpanT和Utf8等现代.NET特性在序列化和反序列化时尤其是在处理大量小对象或大型文档时其速度和内存效率显著优于Newtonsoft.Json。举个例子我们序列化一个包含10000个简单对象的列表// 使用 System.Text.Json var options new JsonSerializerOptions { WriteIndented false }; byte[] utf8Json JsonSerializer.SerializeToUtf8Bytes(largeList, options); // 使用 Newtonsoft.Json string jsonString JsonConvert.SerializeObject(largeList, Formatting.None);在基准测试中JsonSerializer.SerializeToUtf8Bytes不仅速度更快而且直接生成UTF-8字节数组避免了从UTF-16字符串到UTF-8的额外转换对于网络传输尤其友好。反序列化时JsonSerializer.Deserialize同样利用了Utf8JsonReader这个高性能、低分配的阅读器。注意System.Text.Json默认是大小写敏感的而Newtonsoft.Json默认是大小写不敏感的。如果你从Newtonsoft迁移过来接口突然报“反序列化失败”首先就该检查属性名大小写匹配问题。可以通过JsonSerializerOptions.PropertyNameCaseInsensitive true来设置不敏感。2.2 功能与灵活性Newtonsoft.Json 的丰富生态然而Newtonsoft.Json在功能丰富度和灵活性上目前依然领先。许多复杂的、非标准的场景在System.Text.Json中可能需要更多代码才能实现而在Newtonsoft.Json里可能一个属性标签就搞定了。一些典型的、Newtonsoft.Json更易处理的场景包括多态类型序列化处理继承类的集合时Newtonsoft.Json的TypeNameHandling设置可以轻松地在JSON中嵌入类型信息。更强大的自定义转换器Converter虽然两者都支持转换器但Newtonsoft.Json的JsonConverter写起来更直观社区有海量现成的转换器用于处理特殊格式如特殊日期、自定义枚举、DataTable等。对动态类型dynamic、JObject、JArray的无与伦比的支持如果你需要处理结构不确定的JSONNewtonsoft.Json提供的JToken体系JObject,JArray,JValue操作起来就像在JavaScript中一样灵活。更精细的序列化控制比如DefaultValueHandling忽略默认值、NullValueHandling处理空值等配置项非常详尽。2.3 实战选型建议我的经验是新建项目尤其是ASP.NET Core Web API项目优先使用System.Text.Json。它是.NET Core及以后版本的默认库与框架集成度最高如模型绑定、响应输出默认都用它性能好且能满足绝大多数标准RESTful API的需求。遗留项目或需要处理复杂、不规则JSON继续使用Newtonsoft.Json。迁移成本可能很高且其强大功能在复杂场景下不可或缺。在ASP.NET Core中也可以通过安装Microsoft.AspNetCore.Mvc.NewtonsoftJson包并配置服务来继续使用它。高性能中间件或库强烈推荐System.Text.Json特别是利用其底层APIUtf8JsonWriter,Utf8JsonReader进行流式处理可以做到极致的内存控制。3. 核心操作解析序列化与反序列化的实战细节选好了工具我们来深入最核心的两个操作把对象变成字符串序列化/生成以及把字符串变回对象反序列化/解析。3.1 序列化将对象转换为JSON字符串序列化的目标是将一个C#对象图转换成一个符合JSON格式的字符串。这里的关键在于控制输出的格式。使用 System.Text.Jsonpublic class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public DateTime CreatedDate { get; set; } } var product new Product { Id 1, Name Laptop, Price 999.99m, CreatedDate DateTime.UtcNow }; // 基本序列化 string jsonString JsonSerializer.Serialize(product); // 输出{Id:1,Name:Laptop,Price:999.99,CreatedDate:2023-10-27T06:30:00Z} // 带格式化的序列化美化输出常用于调试 var options new JsonSerializerOptions { WriteIndented true }; string prettyJson JsonSerializer.Serialize(product, options); // 输出带缩进和换行的JSON // 自定义属性命名策略如驼峰命名 options.PropertyNamingPolicy JsonNamingPolicy.CamelCase; string camelCaseJson JsonSerializer.Serialize(product, options); // 输出{id:1,name:Laptop,price:999.99,createdDate:2023-10-27T06:30:00Z}使用 Newtonsoft.Jsonstring jsonString JsonConvert.SerializeObject(product, Formatting.Indented); // 同样可以美化输出 var settings new JsonSerializerSettings { ContractResolver new CamelCasePropertyNamesContractResolver(), DateFormatString yyyy-MM-dd // 自定义日期格式 }; string customJson JsonConvert.SerializeObject(product, Formatting.Indented, settings);实操心得在Web API开发中为了与前端JavaScript社区习惯保持一致强烈建议统一使用驼峰命名Camel Case作为JSON的属性名规范。这能减少前后端沟通成本。在System.Text.Json中设置JsonNamingPolicy.CamelCase在Newtonsoft.Json中使用CamelCasePropertyNamesContractResolver即可。3.2 反序列化将JSON字符串转换为对象反序列化是解析的核心你需要确保JSON字符串的结构与你目标对象的类型定义相匹配。使用 System.Text.Jsonstring json {id:1,name:Laptop,price:999.99}; // 基本反序列化 - 注意属性名大小写 var product JsonSerializer.DeserializeProduct(json); // 如果json是驼峰而Product类属性是帕斯卡命名这里会失败除非设置PropertyNameCaseInsensitive true var options new JsonSerializerOptions { PropertyNameCaseInsensitive true // 忽略大小写 }; product JsonSerializer.DeserializeProduct(json, options); // 成功 // 处理不完整的JSONJSON只包含对象的部分属性 string partialJson {id:1,name:Laptop}; product JsonSerializer.DeserializeProduct(partialJson, options); // product.Price将为默认值0不会报错使用 Newtonsoft.Jsonvar product JsonConvert.DeserializeObjectProduct(json); // Newtonsoft.Json 默认就是大小写不敏感的所以上面的json能直接解析到Product类的Id、Name属性。 // 更灵活地处理未知属性 var settings new JsonSerializerSettings { MissingMemberHandling MissingMemberHandling.Error // 如果JSON中有属性在类中不存在则抛出异常 }; try { product JsonConvert.DeserializeObjectProduct(json, settings); } catch (JsonSerializationException ex) { Console.WriteLine($发现了未知属性: {ex.Message}); }处理嵌套对象和集合JSON经常包含数组和嵌套对象反序列化时它们会自然地映射到C#的列表和类属性上。public class Order { public int OrderId { get; set; } public ListProduct Items { get; set; } // 对应JSON数组 public Customer Buyer { get; set; } // 对应嵌套的JSON对象 } string complexJson { orderId: 1001, items: [ {id:1, name:Mouse, price:25.99}, {id:2, name:Keyboard, price:79.99} ], buyer: { customerId: 500, fullName: John Doe } }; var order JsonSerializer.DeserializeOrder(complexJson, new JsonSerializerOptions { PropertyNameCaseInsensitive true }); Console.WriteLine(order.Items.Count); // 输出: 2 Console.WriteLine(order.Buyer.FullName); // 输出: John Doe4. 高级特性与疑难杂症处理掌握了基础我们来看看那些让人头疼的高级场景和常见坑点。4.1 自定义序列化用特性Attribute和转换器Converter控制一切有时默认的序列化行为不符合你的需求。比如你想忽略某个属性、自定义日期格式、或者把一个枚举序列化成字符串而不是数字。使用特性更简单直观using System.Text.Json.Serialization; // 注意命名空间 public class Product { public int Id { get; set; } [JsonPropertyName(product_name)] // 自定义JSON中的属性名 public string Name { get; set; } [JsonIgnore] // 完全忽略此属性不参与序列化和反序列化 public string InternalCode { get; set; } [JsonConverter(typeof(JsonStringEnumConverter))] // 将枚举序列化为字符串 public ProductCategory Category { get; set; } [JsonIgnore(Condition JsonIgnoreCondition.WhenWritingDefault)] // 仅当值为默认值时忽略 public decimal? Discount { get; set; } } public enum ProductCategory { Electronics, Books, Clothing }Newtonsoft.Json也有类似的特性如[JsonProperty]、[JsonIgnore]。编写自定义转换器处理复杂场景当内置特性无法满足时就需要自定义转换器。例如处理一个特殊的日期字符串格式dd/MM/yyyy。System.Text.Json 自定义转换器示例public class CustomDateTimeConverter : JsonConverterDateTime { private readonly string _format; public CustomDateTimeConverter(string format) _format format; public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // 从JSON中读取字符串并按指定格式解析为DateTime var dateString reader.GetString(); return DateTime.ParseExact(dateString, _format, CultureInfo.InvariantCulture); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { // 将DateTime按指定格式写入JSON字符串 writer.WriteStringValue(value.ToString(_format, CultureInfo.InvariantCulture)); } } // 使用方式1通过特性标注在属性上 public class Event { [JsonConverter(typeof(CustomDateTimeConverter), dd/MM/yyyy)] public DateTime EventDate { get; set; } } // 使用方式2在JsonSerializerOptions中全局注册 var options new JsonSerializerOptions(); options.Converters.Add(new CustomDateTimeConverter(dd/MM/yyyy));4.2 处理循环引用与引用相等性这是序列化对象图时的一个经典问题。比如Order对象包含一个Customer而这个Customer对象又有一个Orders列表指向包含它的Order这就构成了循环引用。默认序列化器会陷入无限循环导致栈溢出。Newtonsoft.Json 的解决方案var settings new JsonSerializerSettings { ReferenceLoopHandling ReferenceLoopHandling.Ignore // 忽略循环引用在引用处输出null // 或者 ReferenceLoopHandling ReferenceLoopHandling.Serialize配合 PreserveReferencesHandling }; var json JsonConvert.SerializeObject(orderWithCycle, Formatting.Indented, settings);System.Text.Json 的解决方案System.Text.Json默认不支持循环引用且没有内置的ReferenceLoopHandling。这是其设计上为了性能和安全性做的取舍。如果你的数据结构确实存在循环引用你有几个选择重新设计数据结构这是最根本的解决方案考虑使用DTO数据传输对象在序列化时打破循环只传递必要的数据。使用[JsonIgnore]特性在构成循环的某个属性上标记[JsonIgnore]手动切断循环。考虑换用Newtonsoft.Json如果数据结构复杂且无法更改在ASP.NET Core中集成Newtonsoft.Json可能是更务实的选择。4.3 动态JSON与匿名类型处理当你面对结构未知或变化的JSON时强类型反序列化就不好用了。这时需要动态解析。使用 System.Text.Json 的 JsonDocument 和 JsonElement这是高性能、低分配的选择适合只读访问。string jsonString {\name\: \John\, \age\: 30, \hobbies\: [\reading\, \gaming\]}; using JsonDocument doc JsonDocument.Parse(jsonString); JsonElement root doc.RootElement; string name root.GetProperty(name).GetString(); int age root.GetProperty(age).GetInt32(); JsonElement hobbies root.GetProperty(hobbies); foreach (JsonElement hobby in hobbies.EnumerateArray()) { Console.WriteLine(hobby.GetString()); } // 注意JsonDocument是Disposable的使用using语句管理生命周期。使用 Newtonsoft.Json 的 JToken 体系功能更强大支持动态修改语法更接近JavaScript。string jsonString ...; JObject jObject JObject.Parse(jsonString); string name (string)jObject[name]; int age (int)jObject[age]; // 动态访问即使属性不存在也不会立即报错 var maybeValue jObject[address]?[city]?.ToString(); // 动态修改和创建 jObject[newField] new value; JArray newArray new JArray { item1, item2 }; jObject.Add(dynamicArray, newArray); string modifiedJson jObject.ToString();反序列化到匿名类型或字典对于结构已知但不想创建完整类的情况可以反序列化到dynamicNewtonsoft或匿名类型、字典。// System.Text.Json 反序列化到字典 var dict JsonSerializer.DeserializeDictionarystring, object(jsonString); var name dict[name].ToString(); // Newtonsoft.Json 反序列化到 dynamic dynamic dynamicObj JsonConvert.DeserializeObject(jsonString); Console.WriteLine(dynamicObj.name);4.4 性能优化与异步流处理处理几MB甚至几百MB的JSON文件时性能至关重要。使用流式APISystem.Text.Json避免一次性将整个JSON字符串加载到内存。// 异步从文件流反序列化 await using FileStream fs File.OpenRead(largefile.json); var data await JsonSerializer.DeserializeAsyncListMyData(fs, options); // 异步序列化到文件流 await using FileStream writeFs File.CreateWrite(output.json); await JsonSerializer.SerializeAsync(writeFs, largeDataList, options);使用 Utf8JsonReader/Utf8JsonWriter 进行手动、极致的控制这是最高性能、最低内存分配的方式但代码最复杂。适合编写自己的高性能解析器或处理非标准JSON。// 这是一个简化示例手动读取一个JSON数组中的某个特定字段 ReadOnlySpanbyte jsonUtf8 Encoding.UTF8.GetBytes(jsonString); // 实际中可能来自网络流 var reader new Utf8JsonReader(jsonUtf8); while (reader.Read()) { if (reader.TokenType JsonTokenType.PropertyName reader.ValueTextEquals(targetField)) { reader.Read(); // 移动到属性值 if (reader.TokenType JsonTokenType.String) { string value reader.GetString(); // 处理找到的值 } } }5. 常见问题排查与调试技巧即使掌握了所有API在实际开发中你还是会遇到各种奇怪的问题。这里记录一些最常见的坑和排查方法。5.1 反序列化失败原因分析与快速定位当Deserialize抛出JsonException时别慌按以下步骤排查错误现象可能原因排查方法JsonException: The JSON value could not be converted to...类型不匹配。例如JSON中是字符串123但C#属性是int。1. 检查JSON和C#类的属性类型是否一致。2. 使用JsonElement的GetRawText()或ToString()查看实际解析到的值。3. 考虑使用可为空类型int?或自定义转换器。JsonException: ‘$’ is an invalid start of a value.JSON格式错误如缺少引号、括号不匹配、存在非法字符。1. 将JSON字符串粘贴到在线JSON校验器如 jsonlint.com检查语法。2. 检查字符串中是否包含未转义的控制字符或换行符。反序列化后属性为null或默认值1. JSON中缺少该属性。2. 属性名大小写不匹配System.Text.Json默认敏感。3. 属性有自定义setter且逻辑导致未赋值。1. 确认JSON字符串是否包含该属性。2. 设置PropertyNameCaseInsensitive true。3. 检查属性的set访问器内部逻辑。循环引用导致栈溢出Newtonsoft对象图中存在循环引用。配置ReferenceLoopHandling.Ignore。对于System.Text.Json需重构模型或忽略属性。调试利器使用JsonDocument或JObject进行探索性解析当不确定JSON结构时先把它解析成JsonDocument或JObject然后遍历其节点打印出键和值的类型这能帮你快速理解数据结构。在Newtonsoft.Json中启用跟踪TraceWriter这在诊断复杂序列化问题时非常有用。MemoryTraceWriter traceWriter new MemoryTraceWriter(); var settings new JsonSerializerSettings { TraceWriter traceWriter }; var result JsonConvert.DeserializeObjectMyClass(json, settings); Console.WriteLine(traceWriter.ToString()); // 输出详细的序列化日志5.2 日期与时间格式的永恒之战日期时间是跨系统交互的常见痛点。JSON标准没有定义日期格式通常使用ISO 8601字符串如2023-10-27T06:30:00Z。System.Text.Json默认序列化为ISO 8601格式。你可以通过JsonSerializerOptions.Converters添加自定义的DateTimeConverter来改变格式。Newtonsoft.Json默认也是ISO 8601但可以通过DateFormatString设置轻松自定义如yyyy-MM-dd。重要提醒在处理日期时务必明确时区。最佳实践是在业务逻辑内部统一使用DateTime.UtcNow获取时间序列化时使用UTC时间带Z后缀并在前端或消费者端根据需要进行时区转换。这可以避免因服务器所在地时区不同而导致的混乱。5.3 处理特殊字符与编码JSON字符串中的特殊字符如引号、反斜杠\、换行符\n等需要进行转义。两个库都会自动处理这些转义。但需要注意如果你从一些非标准源如拼接的字符串、某些旧系统获取“JSON”可能会遇到未转义的控制字符。这时在反序列化前你可能需要先进行字符串清洗或使用更宽松的解析模式Newtonsoft.Json的JsonLoadSettings可以设置一些容错。关于编码JSON标准规定使用UTF-8、UTF-16或UTF-32。System.Text.Json强烈推荐并优化了UTF-8的处理。当从文件或网络流中读取时确保使用正确的编码。StreamReader默认使用UTF-8这通常是安全的。5.4 属性缺失与默认值处理有时JSON可能只包含对象的部分字段。反序列化时缺失的属性会被设置为C#类型的默认值如int为0string为nullbool为false。如何区分“提供的null”和“缺失的属性”在System.Text.Json中如果JSON中明确设置了nullproperty: null并且你的C#属性是可为空的如string?或int?那么反序列化后该属性值为null。如果JSON中完全没出现这个属性那么该属性保持其初始值如果没在构造函数中初始化则是默认值。要严格区分可能需要使用JsonElement进行手动解析或自定义转换器。忽略默认值序列化为了减少JSON体积你可能不想序列化那些等于默认值的属性。System.Text.Json:JsonSerializerOptions.DefaultIgnoreCondition JsonIgnoreCondition.WhenWritingDefaultNewtonsoft.Json:DefaultValueHandling DefaultValueHandling.Ignore6. 实战案例构建一个健壮的配置读取器让我们用一个综合案例把上面的知识点串起来。假设我们有一个应用的配置文件appsettings.json结构相对复杂并且我们需要在程序启动时将其读入一个强类型配置对象。配置文件示例 (appsettings.json):{ AppName: MyService, Logging: { Level: Information, FilePath: /var/log/myservice.log, RetentionDays: 30 }, Features: { EnableCache: true, CacheDurationMinutes: 5 }, Endpoints: [ { Name: Primary, Url: https://api.primary.example.com, TimeoutSeconds: 30 }, { Name: Fallback, Url: https://api.fallback.example.com, TimeoutSeconds: 60 } ] }对应的C#配置类public class AppConfig { public string AppName { get; set; } public LoggingConfig Logging { get; set; } public FeaturesConfig Features { get; set; } public ListEndpointConfig Endpoints { get; set; } } public class LoggingConfig { public string Level { get; set; } public string FilePath { get; set; } public int RetentionDays { get; set; } } public class FeaturesConfig { public bool EnableCache { get; set; } public int CacheDurationMinutes { get; set; } } public class EndpointConfig { public string Name { get; set; } public string Url { get; set; } public int TimeoutSeconds { get; set; } }健壮的配置加载代码使用 System.Text.Jsonusing System.Text.Json; public class ConfigurationLoader { private readonly JsonSerializerOptions _options; public ConfigurationLoader() { _options new JsonSerializerOptions { PropertyNameCaseInsensitive true, // 忽略大小写更健壮 ReadCommentHandling JsonCommentHandling.Skip, // 跳过JSON中的注释 AllowTrailingCommas true, // 允许末尾逗号 // 可以在这里添加自定义转换器例如处理特殊日期格式 }; } public AppConfig LoadConfig(string filePath) { if (!File.Exists(filePath)) { throw new FileNotFoundException($配置文件未找到: {filePath}); } try { // 使用异步流式读取适合大文件 using FileStream fileStream File.OpenRead(filePath); var config JsonSerializer.DeserializeAsyncAppConfig(fileStream, _options).GetAwaiter().GetResult(); // 同步上下文中使用 // 基础验证 if (config null) { throw new InvalidOperationException(反序列化配置对象为null。); } if (string.IsNullOrEmpty(config.AppName)) { // 可以设置默认值或抛出更具体的异常 config.AppName DefaultAppName; // 或者: throw new InvalidDataException(AppName 是必须的配置项。); } // 可以添加更多业务逻辑验证如Endpoints列表不能为空等 if (config.Endpoints?.Any() ! true) { config.Endpoints new ListEndpointConfig { new EndpointConfig { Name Default, Url http://localhost, TimeoutSeconds 30 } }; } return config; } catch (JsonException jsonEx) { // 捕获JSON解析错误包装成更有意义的异常 throw new InvalidOperationException($配置文件 {filePath} 格式无效。详细信息: {jsonEx.Message}, jsonEx); } catch (Exception ex) when (ex is not FileNotFoundException) { // 处理其他异常 throw new InvalidOperationException($加载配置文件 {filePath} 时发生未知错误。, ex); } } // 可选提供一个动态查看配置的方法用于调试 public string InspectConfig(string filePath) { using JsonDocument doc JsonDocument.Parse(File.ReadAllText(filePath)); using var stream new MemoryStream(); using var writer new Utf8JsonWriter(stream, new JsonWriterOptions { Indented true }); doc.RootElement.WriteTo(writer); writer.Flush(); return Encoding.UTF8.GetString(stream.ToArray()); } }使用示例var loader new ConfigurationLoader(); try { AppConfig config loader.LoadConfig(appsettings.json); Console.WriteLine($应用名: {config.AppName}); Console.WriteLine($缓存是否启用: {config.Features.EnableCache}); Console.WriteLine($第一个端点: {config.Endpoints[0].Name} - {config.Endpoints[0].Url}); } catch (Exception ex) { Console.WriteLine($加载配置失败: {ex.Message}); // 可以在这里加载默认配置或终止程序 }这个案例展示了如何将JSON解析融入到一个实际的、健壮的组件中。它包含了错误处理、默认值设置、配置验证以及使用JsonDocument进行调试的实用技巧。在实际项目中ASP.NET Core的配置系统已经基于System.Text.Json或Newtonsoft.Json做了非常完善的封装IConfiguration但其底层原理与我们这里所探讨的完全一致。理解这些底层操作能让你在需要自定义配置源或处理特殊格式时游刃有余。