C# JSON序列化实战:Newtonsoft.Json从入门到性能优化
2026/8/15 2:20:34 网站建设 项目流程

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与序列化基础认知

在深入代码之前,我们得先统一认知。JSON(JavaScript 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.1234567+08: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.DeserializeObject<SensorData>(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#中的集合类型,如List<T>数组 T[]等,处理起来同样直接。

// 序列化集合 List<SensorData> dataList = new List<SensorData> { 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}]"; List<SensorData> list = JsonConvert.DeserializeObject<List<SensorData>>(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为默认值0,Timestamp为DateTime.MinValue string json = JsonConvert.SerializeObject(dataWithNull, settings); // 输出:{"DeviceId":"E5"}。Temperature为0(数值默认值),Humidity为0.0,Timestamp为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 List<Order> Orders { get; set; } = new List<Order>(); } 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 和 JToken

Newtonsoft.Json提供了JObjectJArrayJToken等类型来表示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 JSON

JToken家族完美支持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。更安全的做法是使用JTokenSelectToken方法,它支持JSONPath查询,并且可以安全地处理路径不存在的情况:var token = jobj.SelectToken("$.data.sensors[0].value"); if (token != null) { ... }

5. 性能优化与异常处理实战

当处理大量数据或高频请求时(例如一个实时接收MQTT数据并入库的服务),JSON序列化的性能就成了必须考虑的因素。同时,健壮的异常处理机制是保证程序稳定性的关键。

5.1 序列化性能优化要点

  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(); }
  2. 使用流式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.Deserialize<MyModel>(jsonReader); // 处理obj,例如分批存入数据库 } } }

    这种方式能极大降低内存峰值,避免程序因内存不足而崩溃。在处理日志文件、数据导出文件时非常有效。

  3. 谨慎使用特性:每个[JsonProperty]特性在反射时都有开销。对于性能极度敏感的场景,可以考虑使用契约解析器(IContractResolver)在全局层面定义规则,而不是在每个属性上标记特性。

5.2 健壮的异常处理与数据验证

反序列化失败是家常便饭,原因五花八门:数据格式错误、类型不匹配、缺少必需字段等。

string malformedJson = @"{'DeviceId': 'SN-001', 'Temperature': 'not_a_number'}"; // Temperature应该是数字 try { var data = JsonConvert.DeserializeObject<SensorData>(malformedJson); } catch (JsonSerializationException ex) { Console.WriteLine($"反序列化失败: {ex.Message}"); // 记录日志,返回错误信息给客户端等 } catch (JsonReaderException ex) { Console.WriteLine($"JSON格式错误: {ex.Message}"); // 通常是JSON字符串本身语法有问题 }

除了捕获异常,更主动的做法是进行数据验证。可以利用JsonSerializerSettingsError事件来处理错误,而不是让程序抛出异常中断。

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.DeserializeObject<SensorData>(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 如何选择?

根据我多年的项目经验,可以遵循以下原则:

  1. 新项目,且需求标准:如果你的项目是基于.NET 6/7/8的新项目,并且JSON处理需求比较标准(简单的API序列化、配置读取),优先使用System.Text.Json。它能减少外部依赖,享受更好的性能,并且是微软主推的方向。

  2. 遗留项目或复杂需求:如果你的项目是旧项目,已经大量使用了Json.NET,或者你有以下复杂需求,那么坚持使用Json.NET是更明智的选择:

    • 需要处理多态类型(序列化接口或基类,反序列化时得到具体子类)。
    • 需要高度自定义的序列化逻辑(如基于业务规则的属性转换)。
    • 需要方便地处理动态或未知结构的JSONJObject/LINQ to JSON目前仍比System.Text.JsonJsonNode更成熟易用)。
    • 需要处理循环引用,并且希望有开箱即用的解决方案。
    • 依赖大量使用了Json.NET特性的第三方库。
  3. 混合使用:在一些大型项目中,也可能出现混合使用的情况。例如,对性能要求极高的内部模块使用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进行映射。
  • 检查属性Setter:C#属性必须有publicset访问器(或构造函数参数匹配),否则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或利用JsonSerializerSettingsTraceWriter属性(虽然已过时,但在调试时有用)来输出序列化/反序列化的详细步骤日志,这对于追踪复杂对象的转换过程非常有帮助。

掌握Newtonsoft.Json,远不止是学会几个API调用。它关乎如何在C#生态中高效、稳健地处理数据交换这一核心任务。从简单的配置读写到复杂的动态数据解析,从性能优化到异常防御,每一个细节都影响着应用程序的稳定性和开发者的效率。希望这篇结合了大量实战经验的总结,能成为你手边一份可靠的参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询