C# JSON序列化实战:Newtonsoft.Json从入门到性能优化

📅 发布时间:2026/8/15 2:20:38
C# JSON序列化实战:Newtonsoft.Json从入门到性能优化
1. 项目概述为什么C#开发者绕不开Newtonsoft.Json在C#的世界里处理JSON数据就像吃饭喝水一样平常。无论是开发Web API、桌面应用还是做数据交换、配置文件解析你总会遇到需要把对象序列化成JSON字符串或者把一串JSON文本反序列化成可操作对象的情况。虽然.NET Core 3.0之后微软推出了内置的System.Text.Json但直到今天Newtonsoft.Json也就是大家常说的Json.NET依然是无数C#项目尤其是遗留项目和复杂场景下的“定海神针”。我自己在十多年的开发经历中从早期的WCF服务到现在的微服务架构Json.NET几乎参与了每一个需要数据序列化的环节。它之所以能经久不衰核心在于其无与伦比的灵活性和强大的功能。内置的System.Text.Json追求的是极致的性能和对新框架的原生支持而Json.NET则像一把瑞士军刀提供了你能想到的几乎所有处理JSON的需求复杂的类型转换、自定义序列化规则、忽略循环引用、处理日期格式、动态对象操作等等。对于刚接触C#的新手学会使用Json.NET是迈向“高级编程”的必经之路对于老手深入理解其高级特性则能让你在解决诸如“MQTT服务器接收的异构数据入库”、“处理第三方API返回的不规范JSON”这类棘手问题时游刃有余。这篇文章我就结合自己踩过的坑和积累的经验带你从入门到精通彻底玩转Newtonsoft.Json。2. 核心概念与基础操作解析2.1 JSON与序列化基础认知在深入代码之前我们得先统一认知。JSONJavaScript Object Notation是一种轻量级的数据交换格式它基于文本易于人阅读和编写也易于机器解析和生成。在C#中我们通常将“对象转换成JSON字符串”的过程称为序列化Serialize而将“JSON字符串转换回对象”的过程称为反序列化Deserialize。这是所有操作的基石。Newtonsoft.Json的核心是一个名为JsonConvert的静态工具类以及JsonSerializerSettings这个用于控制序列化行为的配置类。绝大多数基础操作通过JsonConvert就能完成。2.2 基础序列化与反序列化实战让我们从一个最简单的实体类开始。假设我们正在开发一个上位机应用需要处理从设备传来的传感器数据。public class SensorData { public string DeviceId { get; set; } public double Temperature { get; set; } public double Humidity { get; set; } public DateTime Timestamp { get; set; } }序列化对象到JSON字符串SensorData data new SensorData { DeviceId SN-001, Temperature 25.6, Humidity 60.2, Timestamp DateTime.Now }; string jsonString JsonConvert.SerializeObject(data); Console.WriteLine(jsonString); // 输出类似{DeviceId:SN-001,Temperature:25.6,Humidity:60.2,Timestamp:2023-10-27T10:30:00.123456708:00}这里SerializeObject方法自动将对象的公有属性转换成了JSON的键值对。默认情况下日期会被转换成ISO 8601格式的字符串。反序列化JSON字符串到对象string incomingJson {DeviceId:SN-002,Temperature:22.1,Humidity:55.8,Timestamp:2023-10-27T10:35:00Z}; SensorData receivedData JsonConvert.DeserializeObjectSensorData(incomingJson); Console.WriteLine($设备 {receivedData.DeviceId} 温度{receivedData.Temperature}°C);反序列化时Json.NET会尝试将JSON中的键名与目标类SensorData的属性名进行匹配默认区分大小写。如果JSON字符串中缺少某个属性对应属性会保持默认值如数字为0引用类型为null如果JSON中多出了属性默认情况下Json.NET会忽略它们而不会报错。这个特性在处理版本不一致或来自不同来源的API数据时非常有用。注意在实际项目中尤其是像处理MQTT消息或第三方接口数据时你拿到的JSON字符串可能包含无法预料的字段。Json.NET默认的“忽略多余字段”行为相比一些严格解析的库如早期的JavaScriptSerializer大大提高了代码的健壮性避免了因接口微调而导致程序崩溃的问题。2.3 处理集合与数组JSON数组对应C#中的集合类型如ListT、数组 T[]等处理起来同样直接。// 序列化集合 ListSensorData dataList new ListSensorData { new SensorData { DeviceId A1, Temperature 20.0 }, new SensorData { DeviceId B2, Temperature 21.5 } }; string jsonArray JsonConvert.SerializeObject(dataList); // 输出[{DeviceId:A1,Temperature:20.0,...}, {...}] // 反序列化集合 string jsonArrayString [{DeviceId:C3,Temperature:19.8}, {DeviceId:D4,Temperature:23.0}]; ListSensorData list JsonConvert.DeserializeObjectListSensorData(jsonArrayString);这个特性在需要将一批配置如从JSON文件读取的TVBox接口配置、书源列表加载到内存中时非常方便。3. 高级配置与自定义序列化基础操作只能应对标准情况。一旦遇到日期格式不兼容、需要忽略某些属性、或者处理循环引用等复杂场景就必须请出JsonSerializerSettings了。它是Json.NET强大功能的控制中枢。3.1 控制日期格式与空值处理不同系统对日期格式的要求天差地别。数据库可能用DateTime前端可能用时间戳而某些老旧设备传来的数据可能是27/10/2023这样的字符串。var settings new JsonSerializerSettings { // 1. 日期格式使用自定义格式字符串 DateFormatString yyyy-MM-dd HH:mm:ss, // 2. 将DateTime转换为UTC时间后再序列化 DateTimeZoneHandling DateTimeZoneHandling.Utc, // 3. 如何处理空值 NullValueHandling NullValueHandling.Ignore, // 忽略为null的属性不输出到JSON DefaultValueHandling DefaultValueHandling.Ignore // 忽略默认值如int的0 }; SensorData dataWithNull new SensorData { DeviceId E5, Temperature 0 }; // Humidity为默认值0Timestamp为DateTime.MinValue string json JsonConvert.SerializeObject(dataWithNull, settings); // 输出{DeviceId:E5}。Temperature为0数值默认值Humidity为0.0Timestamp为MinValue都被忽略了。NullValueHandling.Ignore在构建API响应时特别有用可以避免传输大量无意义的null字段减少数据量。而DateFormatString则能确保与那些要求特定日期格式的第三方系统比如一些SAP或老旧ERP接口无缝对接。3.2 解决循环引用与命名策略在对象模型中两个类互相引用是非常常见的比如Order类包含Customer属性而Customer类又有Orders属性列表。直接序列化会导致堆栈溢出。public class Order { public Customer Buyer { get; set; } } public class Customer { public ListOrder Orders { get; set; } new ListOrder(); } Customer customer new Customer(); Order order new Order { Buyer customer }; customer.Orders.Add(order); // 构成了循环引用 var settings new JsonSerializerSettings { ReferenceLoopHandling ReferenceLoopHandling.Ignore // 遇到循环引用时忽略它 }; string json JsonConvert.SerializeObject(order, settings); // 序列化成功但在Buyer的Orders属性处会停止深入。另一种更优雅的方式是使用ReferenceLoopHandling.Serialize并配合PreserveReferencesHandling它会为对象生成$id和$ref标识符来保持引用关系但这会使JSON结构变得复杂通常只在需要完整保留对象图的特定场景下使用。命名策略则用于统一JSON属性名的大小写风格。例如C#属性名是帕斯卡命名法DeviceId但某些API要求驼峰命名法deviceId。var settings new JsonSerializerSettings { ContractResolver new CamelCasePropertyNamesContractResolver() // 将所有属性名转为驼峰式 }; string json JsonConvert.SerializeObject(data, settings); // 输出{deviceId:SN-001,temperature:25.6,...}3.3 使用特性进行精细控制除了全局设置你还可以在模型类上使用特性Attribute进行更精细的控制。这是最推荐的方式因为它将序列化规则与数据模型本身绑定在一起意图清晰。using Newtonsoft.Json; public class ConfigModel { [JsonProperty(device_id)] // 序列化后JSON中的键名改为device_id public string DeviceId { get; set; } [JsonIgnore] // 完全忽略此属性不参与序列化和反序列化 public string SecretKey { get; set; } [JsonProperty(Required Required.Always)] // 反序列化时此属性必须存在于JSON中否则抛出异常 public string RequiredField { get; set; } [JsonProperty(Order 1)] // 控制属性在JSON对象中出现的顺序 public int Priority { get; set; } }[JsonProperty]特性功能非常强大。Required属性在验证API请求参数时非常有用可以第一时间发现数据缺失。Order属性则在一些对JSON键顺序有严格要求的场景例如某些基于JSON的签名算法下是必需的。4. 动态与LINQ to JSON处理未知结构数据并非所有JSON结构都能对应到预先定义好的C#类。特别是在开发像“TVBox配置接口”或“书源JSON解析”这类工具时你面对的数据结构可能是动态变化的或者你只关心其中的一小部分。这时动态类型和LINQ to JSON就派上用场了。4.1 使用 dynamic 和 JTokenNewtonsoft.Json提供了JObject、JArray、JToken等类型来表示JSON结构它们都继承自JToken。你可以把它们想象成一个DOM树。string complexJson { status: success, data: { sensors: [ {id: 1, value: 23.4}, {id: 2, value: 24.1} ], timestamp: 1698384600 } }; // 方法1使用 dynamic最方便但无编译时检查 dynamic dynamicObj JObject.Parse(complexJson); Console.WriteLine($状态: {dynamicObj.status}); Console.WriteLine($第一个传感器值: {dynamicObj.data.sensors[0].value}); // 方法2使用强类型的 JToken更安全可进行复杂查询 JObject jobj JObject.Parse(complexJson); string status (string)jobj[status]; double firstValue (double)jobj[data][sensors][0][value];使用dynamic写起来非常简洁像写JavaScript一样。但代价是失去了编译时类型安全和IDE的智能提示如果属性名拼写错误要到运行时才会抛出异常。对于确定性的、需要频繁访问的数据建议使用JToken的索引器方式。4.2 强大的LINQ to JSONJToken家族完美支持LINQ查询这让你能像操作内存集合一样灵活地查询和转换JSON数据。JObject config JObject.Parse(complexJson); // 查询所有传感器ID大于1的数据 var highValueSensors config[data][sensors] .Where(s (int)s[id] 1) .Select(s new { Id (int)s[id], Val (double)s[value] }); foreach (var sensor in highValueSensors) { Console.WriteLine($传感器{sensor.Id}: {sensor.Val}); } // 修改JSON数据 config[data][timestamp] DateTimeOffset.UtcNow.ToUnixTimeSeconds(); config[metadata] new JObject { [version] 1.0 }; // 添加新节点 // 将修改后的JObject转换回字符串 string updatedJson config.ToString(Formatting.Indented);这个功能在需要“过滤-转换”JSON数据的场景下极其高效。比如从一个庞大的书源JSON中快速提取出特定格式或特定网站的书源列表。实操心得在处理来自网络如爬虫或第三方API的JSON时永远不要相信数据的完整性。使用JToken的索引器如jobj[data]时如果键不存在会返回null。而使用dynamic访问不存在的属性会抛出RuntimeBinderException。更安全的做法是使用JToken的SelectToken方法它支持JSONPath查询并且可以安全地处理路径不存在的情况var token jobj.SelectToken($.data.sensors[0].value); if (token ! null) { ... }。5. 性能优化与异常处理实战当处理大量数据或高频请求时例如一个实时接收MQTT数据并入库的服务JSON序列化的性能就成了必须考虑的因素。同时健壮的异常处理机制是保证程序稳定性的关键。5.1 序列化性能优化要点重用 JsonSerializer对于需要反复序列化/反序列化的场景创建并重用JsonSerializer实例比每次都使用JsonConvert的静态方法性能更好因为可以避免重复创建和配置序列化设置的开销。var serializer JsonSerializer.CreateDefault(); // 使用默认配置创建 // 或者使用自定义配置 var settings new JsonSerializerSettings { /* ... */ }; var customSerializer JsonSerializer.Create(settings); using (var stringWriter new StringWriter()) using (var jsonWriter new JsonTextWriter(stringWriter)) { serializer.Serialize(jsonWriter, largeDataList); string json stringWriter.ToString(); }使用流式API处理大JSON对于非常大的JSON文件几百MB甚至GB级别不要一次性将整个字符串读入内存。可以使用JsonTextReader进行流式读取。using (var streamReader new StreamReader(huge.json)) using (var jsonReader new JsonTextReader(streamReader)) { while (jsonReader.Read()) { if (jsonReader.TokenType JsonToken.StartObject) { // 读取单个对象 var obj serializer.DeserializeMyModel(jsonReader); // 处理obj例如分批存入数据库 } } }这种方式能极大降低内存峰值避免程序因内存不足而崩溃。在处理日志文件、数据导出文件时非常有效。谨慎使用特性每个[JsonProperty]特性在反射时都有开销。对于性能极度敏感的场景可以考虑使用契约解析器IContractResolver在全局层面定义规则而不是在每个属性上标记特性。5.2 健壮的异常处理与数据验证反序列化失败是家常便饭原因五花八门数据格式错误、类型不匹配、缺少必需字段等。string malformedJson {DeviceId: SN-001, Temperature: not_a_number}; // Temperature应该是数字 try { var data JsonConvert.DeserializeObjectSensorData(malformedJson); } catch (JsonSerializationException ex) { Console.WriteLine($反序列化失败: {ex.Message}); // 记录日志返回错误信息给客户端等 } catch (JsonReaderException ex) { Console.WriteLine($JSON格式错误: {ex.Message}); // 通常是JSON字符串本身语法有问题 }除了捕获异常更主动的做法是进行数据验证。可以利用JsonSerializerSettings的Error事件来处理错误而不是让程序抛出异常中断。var settings new JsonSerializerSettings { Error (sender, args) { // args.ErrorContext.Error 包含了具体的异常 Console.WriteLine($在路径 {args.ErrorContext.Path} 处发生错误: {args.ErrorContext.Error.Message}); // 标记错误已处理序列化/反序列化过程会继续 args.ErrorContext.Handled true; // 你可以在这里为出错的属性设置一个默认值 if (args.ErrorContext.Path Temperature) { // 假设我们正在反序列化一个对象 var currentObject args.CurrentObject as SensorData; if (currentObject ! null) { currentObject.Temperature -999; // 设置一个错误码 } } } }; var data JsonConvert.DeserializeObjectSensorData(malformedJson, settings); Console.WriteLine(data.Temperature); // 输出: -999这种方式特别适合处理“脏数据”比如从多个不同厂商设备采集上来的数据格式可能不完全统一。通过错误处理事件你可以优雅地降级赋予默认值或记录错误保证程序主流程不中断。6. 与System.Text.Json的对比与选型随着.NET的演进很多新项目开始使用内置的System.Text.Json。了解两者的区别有助于你在不同场景下做出正确选择。6.1 主要差异点对比特性Newtonsoft.Json (Json.NET)System.Text.Json (.NET Core 3.0)来源与依赖第三方库 (James Newton-King).NET 平台内置无需额外NuGet包性能功能丰富性能良好性能更高尤其在大量小对象处理上优势明显功能丰富度极其丰富支持几乎所有你能想到的场景功能相对基础满足大部分常见需求默认严格性较宽松默认忽略多余字段较严格默认反序列化时多余字段会引发异常自定义灵活性极高通过JsonConverter,ContractResolver等可深度定制自定义相对复杂但也在不断完善循环引用处理原生支持ReferenceLoopHandling默认不支持需通过ReferenceHandler.Preserve配置动态/弱类型支持优秀JObject,dynamic有限主要通过JsonDocument,JsonNode(NET 6)日期格式默认处理ISO 8601ISO 8601但默认不包含时区信息6.2 如何选择根据我多年的项目经验可以遵循以下原则新项目且需求标准如果你的项目是基于.NET 6/7/8的新项目并且JSON处理需求比较标准简单的API序列化、配置读取优先使用System.Text.Json。它能减少外部依赖享受更好的性能并且是微软主推的方向。遗留项目或复杂需求如果你的项目是旧项目已经大量使用了Json.NET或者你有以下复杂需求那么坚持使用Json.NET是更明智的选择需要处理多态类型序列化接口或基类反序列化时得到具体子类。需要高度自定义的序列化逻辑如基于业务规则的属性转换。需要方便地处理动态或未知结构的JSONJObject/LINQ to JSON目前仍比System.Text.Json的JsonNode更成熟易用。需要处理循环引用并且希望有开箱即用的解决方案。依赖大量使用了Json.NET特性的第三方库。混合使用在一些大型项目中也可能出现混合使用的情况。例如对性能要求极高的内部模块使用System.Text.Json而对需要与复杂外部API交互的模块继续使用Json.NET。这时需要注意两者模型类上使用的特性[JsonProperty]vs[JsonPropertyName]不兼容。踩坑记录我曾在一个将项目从 .NET Framework 迁移到 .NET Core 的项目中试图将所有 Json.NET 替换为System.Text.Json。结果发现一个核心功能依赖于一个非常复杂的自定义JsonConverter用System.Text.Json重写极其困难且性能提升在业务场景下并不明显。最终我们决定只在新的、简单的服务中使用System.Text.Json核心旧逻辑保持不变。不要为了替换而替换技术选型要服务于业务需求和开发效率。7. 常见问题排查与调试技巧即使经验丰富在处理JSON时也会遇到各种奇怪的问题。下面是一些常见问题的排查清单和调试技巧。7.1 反序列化后属性为null或默认值这是最常见的问题之一。检查属性名大小写JSON键名默认区分大小写。确保JSON中的键名与C#属性名完全匹配或使用[JsonProperty]特性或CamelCasePropertyNamesContractResolver进行映射。检查属性SetterC#属性必须有public的set访问器或构造函数参数匹配否则Json.NET无法赋值。自动属性{ get; set; }是最安全的。检查JSON数据使用在线的JSON格式化工具如 jsonformatter.org验证你的JSON字符串语法是否正确确保没有多余的逗号、缺失的引号。7.2 日期时间反序列化错误JSON中没有标准的日期类型日期通常以字符串形式传递。明确指定格式如果日期字符串不是ISO 8601格式如2023/10/27必须在JsonSerializerSettings中设置DateFormatString或者使用[JsonProperty]特性的ItemConverterType来指定一个自定义的JsonConverter。处理时区明确你的数据源时区和你系统预期的时区。使用DateTimeZoneHandling设置来统一转换如全部转为UTC。7.3 处理特殊字符和转义JSON字符串中的引号、换行符等需要转义。使用原始字符串字面量在C#中对于包含大量转义字符的JSON字符串使用前缀的逐字字符串会更清晰。// 难以阅读 string json1 {\name\: \O\\Reilly\}; // 更清晰 string json2 {name: O\Reilly};序列化时自动转义JsonConvert.SerializeObject会自动处理特殊字符的转义你不需要手动处理。手动拼接JSON字符串是万恶之源极易出错务必使用序列化方法。7.4 调试与日志记录当问题复杂时需要更深入的洞察。启用类型名称处理在调试多态序列化问题时可以在设置中启用TypeNameHandling这样JSON中会包含.NET类型信息有助于理解反序列化时发生了什么。var settings new JsonSerializerSettings { TypeNameHandling TypeNameHandling.Auto // 或 Objects, All }; // 序列化的JSON中会包含 $type: YourNamespace.YourClass, YourAssembly 这样的字段警告TypeNameHandling存在安全风险如果反序列化的JSON来自不可信源如用户输入攻击者可能利用它执行恶意代码。永远不要对不可信的JSON源使用TypeNameHandling。仅在完全可控的内部通信场景下使用。使用序列化跟踪可以编写一个自定义的JsonConverter或利用JsonSerializerSettings的TraceWriter属性虽然已过时但在调试时有用来输出序列化/反序列化的详细步骤日志这对于追踪复杂对象的转换过程非常有帮助。掌握Newtonsoft.Json远不止是学会几个API调用。它关乎如何在C#生态中高效、稳健地处理数据交换这一核心任务。从简单的配置读写到复杂的动态数据解析从性能优化到异常防御每一个细节都影响着应用程序的稳定性和开发者的效率。希望这篇结合了大量实战经验的总结能成为你手边一份可靠的参考。