
1. 从字符串到对象为什么Newtonsoft.Json是C#开发者的首选如果你用C#写过任何需要和外部系统打交道的程序无论是调用一个Web API、解析配置文件还是把数据存到NoSQL数据库里那你肯定绕不开JSON。这玩意儿现在就是数据交换的“普通话”简单、灵活、人机都容易读。但在C#的世界里处理JSON可不是把字符串切一切那么简单。你手头可能有一串从MQTT服务器收到的JSON数据需要立刻反序列化成对象来处理业务逻辑也可能需要把一个复杂的对象比如带嵌套列表和自定义类型的实体序列化成JSON字符串发给前端或者存进文件。这时候原生的System.Text.Json虽然性能不错但当你需要处理一个字段名不规则、或者日期格式千奇百怪的JSON时就会感到束手束脚。这就是Newtonsoft.Json也叫Json.NET登场的时候了。我干了十多年C#开发从WebForm时代到现在的.NET Core/5/6/7/8Json.NET几乎是我每个项目的标配依赖。它不像一些新出的库那样追求极致的性能但在功能丰富度、灵活性和容错性上至今难有对手。你可以把它理解为一个“瑞士军刀”式的JSON工具包从最简单的JObject.Parse到复杂的自定义序列化契约它都能优雅地处理。很多开源项目、博客系统比如一些需要将文本先抽成JSON再转SQL的AI工具链、甚至是TVBox这类应用的配置接口背后都可能用它来处理JSON数据。所以这篇东西不是官方文档的翻译而是我这些年用Json.NET趟过各种坑之后总结出来的实战指南。我会带你从最基础的解析开始一直讲到那些官方文档里不会写的、但实际开发中天天遇到的“骚操作”和避坑点。目标是让你看完之后不仅能搞定日常的JSON处理还能在遇到那些“奇葩”JSON格式时心里有底知道该掏Json.NET里的哪把“刀”。2. 环境准备与基础概念不止是安装一个NuGet包在开始写代码之前我们得先把场子搭好。虽然现在.NET项目默认会引用System.Text.Json但我们要用的是Newtonsoft.Json。2.1 安装与项目配置最直接的方式就是通过Visual Studio的NuGet包管理器或者.NET CLI来安装。打开你的包管理器控制台输入Install-Package Newtonsoft.Json或者用.NET CLIdotnet add package Newtonsoft.Json安装完成后你的.csproj文件里会多出一行类似PackageReference IncludeNewtonsoft.Json Version13.0.3 /的引用。这里有个小细节对于长期维护的项目我建议在版本号上使用浮动版本比如13.0.*时要谨慎。虽然它能自动获取小版本更新但Json.NET的更新有时会包含细微的行为变化。为了构建的可重现性在生产项目中锁定一个具体的大版本如13.0.3通常是更稳妥的做法。安装好后别忘了在需要使用的类文件开头引入命名空间using Newtonsoft.Json; using Newtonsoft.Json.Linq; // 处理动态JSON或LINQ to JSON时会用到2.2 理解序列化与反序列化这是核心中的核心必须彻底搞明白。序列化 (Serialization) 把内存中的一个C#对象比如一个Person类的实例转换成JSON格式的字符串。这个过程就像是把一本立体复杂的乐高模型说明书拍扁成一张二维的步骤图纸JSON字符串方便传输或存储。反序列化 (Deserialization) 是逆过程。把JSON格式的字符串转换回内存中的C#对象。就像你拿到那张二维图纸能重新拼出那个乐高模型。为什么不用C#自带的举个例子你从某个老旧设备或者第三方API收到一个JSON里面的日期字段长这样2023-10-27T12:00:00.000Z。System.Text.Json对ISO 8601格式的解析很严格但Json.NET通过JsonSerializerSettings可以轻松配置多种日期格式甚至处理那些不规范的日期字符串容错能力高出一大截。这在处理历史数据或对接不规范的外部系统时能省下大量处理字符串的脏活。3. 核心三板斧JsonConvert、JToken与JsonSerializerJson.NET提供了好几套API来处理JSON适应不同场景。掌握它们就像掌握了不同口径的螺丝刀。3.1 JsonConvert最常用的静态工具类JsonConvert提供了一组静态方法适合绝大多数简单的、一次性的序列化/反序列化操作。它的特点是方便快捷。基础序列化与反序列化public class Person { public string Name { get; set; } public int Age { get; set; } public DateTime Birthday { get; set; } } // 序列化 Person person new Person { Name 张三, Age 30, Birthday new DateTime(1993, 5, 20) }; string jsonString JsonConvert.SerializeObject(person); // 输出: {Name:张三,Age:30,Birthday:1993-05-20T00:00:00} // 反序列化 Person deserializedPerson JsonConvert.DeserializeObjectPerson(jsonString); Console.WriteLine(deserializedPerson.Name); // 输出: 张三这里默认的日期格式可能不是你要的。我们可以通过传递一个JsonSerializerSettings对象来定制。常用配置示例JsonSerializerSettings settings new JsonSerializerSettings { // 1. 格式化缩进方便人阅读调试时用生产环境通常去掉以节省空间 Formatting Formatting.Indented, // 2. 处理空值忽略为null的属性 NullValueHandling NullValueHandling.Ignore, // 3. 处理默认值忽略值类型int, DateTime等的默认值 DefaultValueHandling DefaultValueHandling.Ignore, // 4. 日期格式指定自定义格式 DateFormatString yyyy-MM-dd HH:mm:ss, // 5. 参考循环处理对象A引用对象B对象B又引用对象A时避免堆栈溢出 ReferenceLoopHandling ReferenceLoopHandling.Ignore, // 6. 合约解析器可以全局配置命名策略如驼峰命名 ContractResolver new CamelCasePropertyNamesContractResolver() }; string formattedJson JsonConvert.SerializeObject(person, settings);注意ReferenceLoopHandling在处理实体框架EF Core的导航属性时特别有用。直接序列化一个从数据库查询出来的、包含双向导航属性的实体很容易引发循环引用异常。设置为Ignore可以打破循环但会丢失部分数据。更精细的做法是使用[JsonIgnore]特性标记特定的导航属性或者在DTO数据传输对象层面进行序列化。3.2 JToken家族动态查询与操作利器有时候你并不关心完整的对象结构或者JSON的格式不确定比如处理TVBox的配置JSON里面的字段可能会变。这时候JToken、JObject、JArray这一套LINQ to JSON API就派上用场了。它们把JSON解析成一个可遍历、可查询的令牌Token树。string complexJson { name: TVBox Config, version: 2, sources: [ { name: 源A, url: http://a.com }, { name: 源B, url: http://b.com } ], extra: { cache: true, timeout: 30 } }; // 解析为JObject对应JSON对象 JObject config JObject.Parse(complexJson); // 1. 直接通过键名获取值类似字典 string name (string)config[name]; // TVBox Config int version (int)config[version]; // 2 // 2. 访问嵌套对象和数组 JArray sources (JArray)config[sources]; string firstSourceName (string)sources[0][name]; // 源A // 3. 使用LINQ进行查询 var sourceUrls sources.Select(s (string)s[url]).ToList(); // 4. 动态添加或修改属性 config[newKey] newValue; config[extra][timeout] 60; // 5. 将修改后的JObject转回字符串 string modifiedJson config.ToString(Formatting.Indented);这种方式的优点是极其灵活特别适合处理“配置型”JSON或者做JSON数据的转换和裁剪。比如你需要从一个大的JSON响应里只提取出某几个字段的值用JObject配合LINQ会比反序列化整个大对象要高效和简单。3.3 JsonSerializer面向流的高级控制当你需要处理非常大的JSON文件比如几个GB的日志文件或者需要与流Stream直接交互时例如在ASP.NET Core Web API中直接从请求体流式读取JSONJsonSerializer类提供了更细粒度的控制。public async Task ProcessLargeJsonFileAsync(string filePath) { using (StreamReader file File.OpenText(filePath)) using (JsonTextReader reader new JsonTextReader(file)) { JsonSerializer serializer new JsonSerializer(); // 假设文件是一个巨大的JSON对象数组 reader.SupportMultipleContent true; // 允许读取多个连续JSON对象 while (await reader.ReadAsync()) { if (reader.TokenType JsonToken.StartObject) { // 流式反序列化每一个对象 MyDataObject obj serializer.DeserializeMyDataObject(reader); // 处理单个对象然后立即释放引用避免内存暴涨 ProcessSingleObject(obj); } } } }这种方式的内存效率极高因为它不会一次性将整个文件加载到内存中而是像流水线一样逐个令牌Token或逐个对象地处理。在做大型数据处理比如用C#做ETL时这是必备技能。4. 实战进阶处理那些“恼人”的特定场景基础操作会了接下来才是真正体现经验价值的地方。下面这些场景几乎每个项目都会碰到一两个。4.1 处理不规则的JSON键名与自定义映射前端传过来的JSON字段名是user_name但你的C#模型属性叫UserName。或者API返回的JSON里有个字段叫classC#关键字你没法直接用它做属性名。解决方案1使用[JsonProperty]特性这是最直接、最常用的方法。public class User { [JsonProperty(user_name)] // 映射JSON中的user_name键 public string UserName { get; set; } [JsonProperty(class)] public string ClassName { get; set; } // C#属性名可以随意起 [JsonProperty(NullValueHandling NullValueHandling.Ignore)] public string OptionalField { get; set; } // 同时配置该字段忽略null值 }解决方案2自定义合约解析器ContractResolver如果你需要全局的命名规则比如把所有属性名都改成蛇形命名snake_case可以自定义一个解析器。public class SnakeCaseContractResolver : DefaultContractResolver { protected override string ResolvePropertyName(string propertyName) { // 一个简单的驼峰转蛇形实现 return System.Text.RegularExpressions.Regex.Replace(propertyName, ([A-Z]), _$1).ToLower().TrimStart(_); } } // 使用 var settings new JsonSerializerSettings { ContractResolver new SnakeCaseContractResolver() }; var json JsonConvert.SerializeObject(user, settings); // 输出会是{user_name:...,class_name:...}很多第三方库的API要求蛇形命名用这个办法可以一劳永逸。4.2 枚举、日期与特殊类型的序列化枚举默认序列化为数字这通常不利于阅读和调试。我们可以将其序列化为字符串public enum Status { Pending, Active, Inactive } public class Order { public Status OrderStatus { get; set; } } var order new Order { OrderStatus Status.Active }; var json JsonConvert.SerializeObject(order, Formatting.Indented); // 默认输出: {OrderStatus:1} // 使用StringEnumConverter后: {OrderStatus:Active} var settings new JsonSerializerSettings(); settings.Converters.Add(new StringEnumConverter()); json JsonConvert.SerializeObject(order, settings);日期时间的处理更是重灾区。除了前面提到的DateFormatString你还可以使用IsoDateTimeConverter等内置转换器或者完全自定义。public class CustomDateTimeConverter : JsonConverterDateTime { private const string Format dd/MM/yyyy; public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer) { writer.WriteValue(value.ToString(Format)); } public override DateTime ReadJson(JsonReader reader, Type objectType, DateTime existingValue, bool hasExistingValue, JsonSerializer serializer) { return DateTime.ParseExact(reader.Value.ToString(), Format, CultureInfo.InvariantCulture); } } // 在模型属性或全局设置中使用 public class Event { [JsonConverter(typeof(CustomDateTimeConverter))] public DateTime EventDate { get; set; } }4.3 性能优化与异常处理性能优化重用JsonSerializerSettings和JsonSerializer 创建这些对象有一定开销。在Web应用等高频场景下应该将它们创建为单例或静态实例进行重用。使用流式API处理大文件 如前所述用JsonTextReader和JsonTextWriter。避免过度序列化 只序列化需要的数据。使用[JsonIgnore]忽略不需要的属性或者专门为API接口创建轻量级的DTO数据传输对象。考虑System.Text.Json 在.NET Core 3.0的项目中如果场景非常纯粹只做简单的序列化/反序列化且格式可控System.Text.Json在性能上有明显优势。可以对性能敏感的内部模块使用它。异常处理反序列化时JSON格式不对或类型不匹配会抛出JsonSerializationException。永远不要相信外部输入。try { var obj JsonConvert.DeserializeObjectMyType(jsonStringFromApi); } catch (JsonSerializationException ex) { // 记录详细的错误信息包括可能出错的路径Path Console.WriteLine($反序列化失败。路径{ex.Path}, 消息{ex.Message}); // 返回默认值或抛出更友好的业务异常 }更高级的做法是使用JsonSerializerSettings的Error事件在错误发生时进行控制而不是直接抛出异常。var settings new JsonSerializerSettings { Error (sender, args) { // args.ErrorContext.Error 是原始异常 // args.ErrorContext.Path 是出错时的JSON路径 Console.WriteLine($在路径 {args.ErrorContext.Path} 发生错误: {args.ErrorContext.Error.Message}); // 标记错误已处理反序列化会继续对于当前成员会使用默认值 args.ErrorContext.Handled true; } }; var obj JsonConvert.DeserializeObjectMyType(jsonString, settings);5. 避坑指南我踩过的那些“坑”这些经验是文档里不会写的但能让你在关键时刻少掉几根头发。坑1时间戳Unix Time的陷阱有些API返回的日期是Unix时间戳从1970年1月1日开始的秒数或毫秒数一个long型的数字。Json.NET默认不会把它当成日期。{createTime: 1698393600000}如果你用DateTime类型的属性去接会直接报错。解决方法是用一个自定义转换器或者在模型里先用long类型接收然后再手动转换。public class Item { [JsonProperty(createTime)] public long CreateTimeMillis { get; set; } [JsonIgnore] // 不参与序列化/反序列化 public DateTime CreateTime DateTimeOffset.FromUnixTimeMilliseconds(CreateTimeMillis).DateTime; }坑2多态类型的反序列化“$type”问题当你有一个基类Animal和派生类Dog、Cat并且JSON数组中混合了不同类型时直接反序列化ListAnimal会丢失具体的类型信息所有元素都会变成Animal基类。 Json.NET支持通过TypeNameHandling设置来在JSON中嵌入.NET类型信息。var settings new JsonSerializerSettings { TypeNameHandling TypeNameHandling.Auto // 或 Arrays, Objects, All }; string json JsonConvert.SerializeObject(myList, settings); // json中会包含$type:YourNamespace.Dog, YourAssembly这样的字段 var deserializedList JsonConvert.DeserializeObjectListAnimal(json, settings);重要安全警告TypeNameHandling是一个非常强大的功能但也是一个严重的安全风险点。如果反序列化的JSON来源不可信比如来自用户输入或外部请求攻击者可以在$type字段中指定任何程序集中的任何类型导致在反序列化过程中执行任意代码。因此在Web API接收数据时绝对不要使用TypeNameHandling.All或Auto。如果必须用请使用白名单机制通过自定义的SerializationBinder来严格限制允许反序列化的类型。坑3数字字符串与数字的混淆JSON标准里数字就是数字字符串就是字符串。但有些API设计不规范会把本该是数字的ID写成字符串123。如果你的C#属性是int Id反序列化时会失败。你可以把属性类型改为string或者使用JsonConverter进行灵活处理。更简单粗暴但有效的方法是在不确定时先用JObject.Parse看看结构再决定如何建模。坑4忽略大小写匹配Json.NET默认是区分属性名大小写的。如果JSON中的username和你的模型属性UserName不匹配就绑定不上。可以通过设置JsonSerializerSettings的ContractResolver为CamelCasePropertyNamesContractResolver将模型属性名视为驼峰来解决或者更通用地设置JsonProperty的PropertyName。6. 与System.Text.Json的对比与选型建议现在.NET官方力推System.Text.JsonSTJ它性能更好并且避免了Newtonsoft.Json的一些安全风险如前面提到的TypeNameHandling。那是不是该全面转向STJ了呢我的建议是分情况优先使用 System.Text.Json 的场景全新的.NET Core/5项目且JSON处理需求简单、标准。对性能有极致要求的微服务或高频API。你希望减少项目的外部依赖。处理的数据完全可控格式严格遵循规范。坚持使用 Newtonsoft.Json 的场景维护遗留项目大量代码已经基于Json.NET。需要处理复杂、不规范、充满“历史包袱”的JSON格式各种奇怪的日期、数字字符串混用、缺失字段等。需要高度灵活和丰富的功能如复杂的自定义转换器JsonConverter。更强大的LINQ to JSON动态查询JObject,JArray。更精细的序列化过程控制如OnSerializing,OnSerialized等事件。对dynamic类型的完美支持。依赖的大量第三方库如AutoMapper、某些ORM的扩展深度集成了Json.NET。在实际项目中我经常看到两者共存。比如Web API层使用System.Text.Json以获得更好的请求/响应性能而在业务逻辑层或数据处理层因为要对接各种奇奇怪怪的旧系统依然使用Newtonsoft.Json。这完全可行只要注意不要在同一模型上混用两者的特性Attribute就行。7. 一个完整的实战案例解析并转换MQTT消息数据假设我们有一个C#开发的MQTT服务器它从设备端接收JSON格式的遥测数据需要解析后存入数据库。数据格式可能不一致有的设备发的是标准ISO时间有的发的是时间戳。步骤1定义数据模型考虑容错public class TelemetryData { // 设备ID可能以字符串形式发送数字 [JsonProperty(deviceId)] public string DeviceId { get; set; } // 温度值 [JsonProperty(temperature)] public double Temperature { get; set; } // 时间字段可能为字符串或数字时间戳 [JsonProperty(timestamp)] [JsonConverter(typeof(FlexibleDateTimeConverter))] // 使用自定义转换器 public DateTime Timestamp { get; set; } // 其他可能不存在的字段 [JsonProperty(humidity)] public double? Humidity { get; set; } // 使用可空类型 } public class FlexibleDateTimeConverter : JsonConverterDateTime { public override DateTime ReadJson(JsonReader reader, Type objectType, DateTime existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType JsonToken.String) { // 尝试按字符串解析 if (DateTime.TryParse(reader.Value.ToString(), out DateTime dt)) return dt; } else if (reader.TokenType JsonToken.Integer || reader.TokenType JsonToken.Float) { // 尝试按Unix时间戳毫秒解析 long millis Convert.ToInt64(reader.Value); return DateTimeOffset.FromUnixTimeMilliseconds(millis).DateTime; } // 如果都无法解析返回最小值或抛出异常根据业务决定 return DateTime.MinValue; } public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer) { // 序列化时统一输出为ISO格式 writer.WriteValue(value.ToString(o)); } }步骤2在MQTT消息处理回调中解析private static JsonSerializerSettings _settings new JsonSerializerSettings { NullValueHandling NullValueHandling.Ignore, MissingMemberHandling MissingMemberHandling.Ignore // 忽略JSON中多出来的字段 }; public void ProcessMqttMessage(string topic, string payload) { try { // 使用容错设置进行反序列化 var data JsonConvert.DeserializeObjectTelemetryData(payload, _settings); if (data ! null) { // 这里可以加入业务验证比如DeviceId不能为空 if (string.IsNullOrEmpty(data.DeviceId)) { _logger.LogWarning(收到无效数据DeviceId为空。); return; } // 转换后存入数据库这里假设使用EF Core _dbContext.TelemetryRecords.Add(new TelemetryRecord { DeviceId data.DeviceId, Temperature data.Temperature, Humidity data.Humidity, Timestamp data.Timestamp }); _dbContext.SaveChanges(); } } catch (JsonException ex) { // 记录解析失败的原始消息便于排查 _logger.LogError(ex, MQTT消息JSON解析失败。Topic: {Topic}, Payload: {Payload}, topic, payload); } catch (Exception ex) { // 处理其他异常如数据库异常 _logger.LogError(ex, 处理MQTT消息时发生错误。); } }这个案例涵盖了类型转换、容错处理、异常记录和业务整合是Json.NET在真实工业场景如C#上位机开发、物联网数据处理中的一个典型应用。关键在于通过合理的模型设计和转换器使用将不规整的外部数据平滑地转换为你系统内部整洁、强类型的对象让核心业务逻辑可以干净、安全地运行。