Newtonsoft.Json实战指南:C# JSON处理核心技巧与避坑
2026/8/27 23:27:06 网站建设 项目流程

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/8,Json.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 CLI:

dotnet add package Newtonsoft.Json

安装完成后,你的.csproj文件里会多出一行类似<PackageReference Include="Newtonsoft.Json" Version="13.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与JsonSerializer

Json.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.DeserializeObject<Person>(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,里面的字段可能会变)。这时候,JTokenJObjectJArray这一套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中直接从请求体流式读取JSON),JsonSerializer类提供了更细粒度的控制。

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.Deserialize<MyDataObject>(reader); // 处理单个对象,然后立即释放引用,避免内存暴涨 ProcessSingleObject(obj); } } } }

这种方式的内存效率极高,因为它不会一次性将整个文件加载到内存中,而是像流水线一样,逐个令牌(Token)或逐个对象地处理。在做大型数据处理(比如用C#做ETL)时,这是必备技能。

4. 实战进阶:处理那些“恼人”的特定场景

基础操作会了,接下来才是真正体现经验价值的地方。下面这些场景,几乎每个项目都会碰到一两个。

4.1 处理不规则的JSON键名与自定义映射

前端传过来的JSON字段名是user_name,但你的C#模型属性叫UserName。或者API返回的JSON里有个字段叫class(C#关键字),你没法直接用它做属性名。

解决方案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 : JsonConverter<DateTime> { 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 性能优化与异常处理

性能优化:

  1. 重用JsonSerializerSettingsJsonSerializer: 创建这些对象有一定开销。在Web应用等高频场景下,应该将它们创建为单例或静态实例进行重用。
  2. 使用流式API处理大文件: 如前所述,用JsonTextReaderJsonTextWriter
  3. 避免过度序列化: 只序列化需要的数据。使用[JsonIgnore]忽略不需要的属性,或者专门为API接口创建轻量级的DTO(数据传输对象)。
  4. 考虑System.Text.Json: 在.NET Core 3.0+的项目中,如果场景非常纯粹(只做简单的序列化/反序列化,且格式可控),System.Text.Json在性能上有明显优势。可以对性能敏感的内部模块使用它。

异常处理:反序列化时,JSON格式不对或类型不匹配会抛出JsonSerializationException。永远不要相信外部输入。

try { var obj = JsonConvert.DeserializeObject<MyType>(jsonStringFromApi); } catch (JsonSerializationException ex) { // 记录详细的错误信息,包括可能出错的路径(Path) Console.WriteLine($"反序列化失败。路径:{ex.Path}, 消息:{ex.Message}"); // 返回默认值或抛出更友好的业务异常 }

更高级的做法是使用JsonSerializerSettingsError事件,在错误发生时进行控制,而不是直接抛出异常。

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.DeserializeObject<MyType>(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,和派生类DogCat,并且JSON数组中混合了不同类型时,直接反序列化List<Animal>会丢失具体的类型信息,所有元素都会变成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.DeserializeObject<List<Animal>>(json, settings);

重要安全警告TypeNameHandling是一个非常强大的功能,但也是一个严重的安全风险点。如果反序列化的JSON来源不可信(比如来自用户输入或外部请求),攻击者可以在$type字段中指定任何程序集中的任何类型,导致在反序列化过程中执行任意代码。因此,在Web API接收数据时,绝对不要使用TypeNameHandling.AllAuto如果必须用,请使用白名单机制,通过自定义的SerializationBinder来严格限制允许反序列化的类型。

坑3:数字字符串与数字的混淆JSON标准里,数字就是数字,字符串就是字符串。但有些API设计不规范,会把本该是数字的ID写成字符串"123"。如果你的C#属性是int Id,反序列化时会失败。你可以把属性类型改为string,或者使用JsonConverter进行灵活处理。更简单粗暴但有效的方法是,在不确定时,先用JObject.Parse看看结构,再决定如何建模。

坑4:忽略大小写匹配Json.NET默认是区分属性名大小写的。如果JSON中的"username"和你的模型属性UserName不匹配,就绑定不上。可以通过设置JsonSerializerSettingsContractResolverCamelCasePropertyNamesContractResolver(将模型属性名视为驼峰)来解决,或者更通用地,设置JsonPropertyPropertyName

6. 与System.Text.Json的对比与选型建议

现在.NET官方力推System.Text.Json(STJ),它性能更好,并且避免了Newtonsoft.Json的一些安全风险(如前面提到的TypeNameHandling)。那是不是该全面转向STJ了呢?我的建议是分情况:

优先使用 System.Text.Json 的场景:

  1. 全新的.NET Core/5+项目,且JSON处理需求简单、标准。
  2. 对性能有极致要求的微服务或高频API。
  3. 你希望减少项目的外部依赖
  4. 处理的数据完全可控,格式严格遵循规范。

坚持使用 Newtonsoft.Json 的场景:

  1. 维护遗留项目,大量代码已经基于Json.NET。
  2. 需要处理复杂、不规范、充满“历史包袱”的JSON格式(各种奇怪的日期、数字字符串混用、缺失字段等)。
  3. 需要高度灵活和丰富的功能,如:
    • 复杂的自定义转换器(JsonConverter)。
    • 更强大的LINQ to JSON动态查询(JObject,JArray)。
    • 更精细的序列化过程控制(如OnSerializing,OnSerialized等事件)。
    • dynamic类型的完美支持。
  4. 依赖的大量第三方库(如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 : JsonConverter<DateTime> { 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.DeserializeObject<TelemetryData>(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#上位机开发、物联网数据处理)中的一个典型应用。关键在于,通过合理的模型设计和转换器使用,将不规整的外部数据,平滑地转换为你系统内部整洁、强类型的对象,让核心业务逻辑可以干净、安全地运行。

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

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

立即咨询