
1. 从“字符串”到“对象”为什么C#开发者必须掌握JSON解析如果你用C#写过任何需要和外部系统打交道的程序无论是调用一个Web API、读取一个配置文件还是把数据存到NoSQL数据库里那你肯定绕不开JSON。它就像程序员世界里的“普通话”几乎成了数据交换的事实标准。但很多刚入门的C#开发者面对一段JSON字符串第一反应可能就是去网上搜“C# JSON解析”然后复制一段用Newtonsoft.Json也就是Json.NET的代码。这没错但如果你只停留在“能用”的层面可能会在后续开发中踩不少坑。我见过不少项目因为JSON解析处理不当导致接口性能低下、内存泄漏甚至因为日期格式、枚举处理不一致而引发线上bug。JSON解析远不止是JsonConvert.DeserializeObjectT(jsonString)这一行代码那么简单。它涉及到序列化器的选型、性能优化、复杂嵌套结构的处理、自定义转换以及在异步流、Span等现代C#特性下的最佳实践。这篇文章我就以一个老C#码农的身份结合我这些年踩过的坑和积累的经验带你彻底搞懂在C#里解析JSON的方方面面。我们会从最基础的System.Text.Json和Newtonsoft.Json讲起深入到性能敏感场景的优化、处理那些“不标准”的JSON最后再聊聊在真实项目比如你提到的上位机、SCPI编程、API调用里如何优雅地集成JSON解析。目标很简单让你不仅能解析JSON更能“精通”解析写出既健壮又高效的代码。2. 核心武器库System.Text.Json 与 Newtonsoft.Json 的深度对比与选型当你要在C#中处理JSON时首先面临的就是工具选择。现在主要有两大阵营微软官方的System.Text.Json和经久不衰的第三方库Newtonsoft.JsonJson.NET。很多人会问“我该用哪个” 这不是一个非此即彼的问题而是一个需要根据场景权衡的选择。2.1 System.Text.Json为性能和现代C#而生System.Text.Json是从.NET Core 3.0开始引入的它被集成在.NET运行时库中意味着你不需要安装额外的NuGet包。它的设计初衷就是高性能和低内存分配。它的核心优势在于性能卓越底层大量使用SpanT和Utf8直接操作避免了不必要的字符串编码转换和内存分配。在大量序列化/反序列化的场景下性能提升非常明显有时能达到Newtonsoft.Json的2倍甚至更多。内存效率高支持异步流式读写Utf8JsonReader,Utf8JsonWriter可以处理GB级别的JSON文件而无需一次性加载到内存。安全性默认配置更严格有助于避免某些安全反序列化漏洞。但它的“脾气”也更倔强一些默认行为差异属性名默认使用“驼峰命名”CamelCase进行匹配而Newtonsoft.Json默认是“帕斯卡命名”PascalCase。这经常是迁移时第一个遇到的坑。功能“精简”出于性能和安全性考虑它没有实现Newtonsoft.Json的所有功能。例如默认不支持循环引用、不支持非字符串字典键、某些高级的自定义转换器写起来更繁琐。一个简单的反序列化例子展示了默认命名策略的差异using System.Text.Json; public class DeviceReading { public string SensorId { get; set; } // PascalCase 属性名 public double Value { get; set; } } string json {sensorId: temp_01, value: 25.5}; // CamelCase JSON键 // System.Text.Json 默认需要大小写不敏感或显式配置才能匹配 var options new JsonSerializerOptions { PropertyNameCaseInsensitive true // 启用不区分大小写匹配 }; var reading JsonSerializer.DeserializeDeviceReading(json, options); Console.WriteLine(reading.SensorId); // 输出temp_012.2 Newtonsoft.Json功能全面的“瑞士军刀”Newtonsoft.Json历史悠久社区庞大几乎支持你能想到的所有JSON相关场景。它的API设计非常人性化功能丰富。它的强大之处在于极高的灵活性处理循环引用、多态类型序列化、忽略默认值、合并JSON等高级功能开箱即用。丰富的设置通过JsonSerializerSettings可以精细控制序列化的每一个环节满足各种奇葩的接口需求。卓越的容错性对于格式不太规范的JSON比如尾随逗号有更好的容忍度。强大的自定义写自定义JsonConverter相对直观可以处理非常复杂的转换逻辑。当然代价也是有的性能开销为了提供灵活性它在性能上做出了妥协通常比System.Text.Json慢。外部依赖需要从NuGet安装增加了项目依赖。using Newtonsoft.Json; // 同样的类和JSON var reading JsonConvert.DeserializeObjectDeviceReading(json); // 默认就能匹配更宽松 Console.WriteLine(reading.SensorId); // 输出temp_01 // 轻松处理复杂场景如忽略空值 var settings new JsonSerializerSettings { NullValueHandling NullValueHandling.Ignore, DefaultValueHandling DefaultValueHandling.Ignore }; string cleanJson JsonConvert.SerializeObject(reading, settings);2.3 实战选型指南什么时候用什么根据我的经验可以遵循以下原则新建项目尤其是高性能、高吞吐量的服务如Web API、微服务优先选择System.Text.Json。利用其高性能和内置优势从开始就建立良好的性能基线。旧项目迁移或维护如果项目严重依赖Newtonsoft.Json的高级特性且迁移成本高继续使用Newtonsoft.Json是更务实的选择。不要为了迁移而迁移。需要处理非常复杂或“脏”的JSON数据比如接口不规范、需要复杂自定义转换、处理多态类型集合。Newtonsoft.Json通常是更省力的工具。类库开发如果你在开发一个供他人使用的类库需要最大兼容性并且不希望强制用户使用特定的JSON库可以考虑抽象一层或者同时支持两者通过条件编译。但通常类库内部使用System.Text.Json是更现代的选择。注意在ASP.NET Core项目中虽然控制器默认使用System.Text.Json但你仍然可以通过安装Microsoft.AspNetCore.Mvc.NewtonsoftJson包并配置服务来换回Newtonsoft.Json。这通常是为了兼容旧有客户端或使用某些Newtonsoft.Json独占特性。3. 超越基础API高性能解析与流式处理实战当你需要处理日志文件、大数据导出的JSON数组或者从网络流中持续读取JSON消息时一次性将整个JSON字符串反序列化成对象模型可能会耗尽内存。这时就需要用到流式处理Streaming。3.1 使用 System.Text.Json 进行流式读取System.Text.Json提供了Utf8JsonReader和Utf8JsonWriter这两个底层结构体用于高性能、低分配的流式读写。它们直接操作UTF-8编码的字节效率极高。假设我们有一个很大的JSON文件data.json里面包含一个设备读数数组我们想逐条处理而不是全部加载到内存。using System.Text.Json; async Task ProcessLargeJsonFileAsync(string filePath) { await using var fileStream File.OpenRead(filePath); var buffer new byte[4096]; // 缓冲区 // 读取直到遇到开始数组的令牌 [ int bytesRead; while ((bytesRead await fileStream.ReadAsync(buffer)) 0) { // 这里简化了逻辑实际需要更复杂的状态机来解析流中的JSON // 更常见的做法是使用 JsonDocument.ParseAsync 进行部分读取 } }对于大多数场景使用JsonDocument或JsonSerializer.DeserializeAsyncEnumerable是更友好的选择。使用JsonDocument进行DOM式流解析JsonDocument可以将JSON解析成一个只读的文档对象模型DOM它比反序列化成强类型对象更轻量并且支持随机访问元素。它底层也使用了Utf8JsonReader。using System.Text.Json; async Task ProcessJsonDocumentAsync(string filePath) { await using var fileStream File.OpenRead(filePath); // 异步解析整个流但JsonDocument本身是轻量级的不代表整个文件都在内存中展开为对象树 using JsonDocument document await JsonDocument.ParseAsync(fileStream); JsonElement root document.RootElement; // 假设根元素是一个数组 if (root.ValueKind JsonValueKind.Array) { foreach (JsonElement element in root.EnumerateArray()) { // 逐条处理每个设备读数对象 if (element.TryGetProperty(sensorId, out JsonElement sensorIdProp) element.TryGetProperty(value, out JsonElement valueProp)) { string sensorId sensorIdProp.GetString(); double value valueProp.GetDouble(); Console.WriteLine($Sensor: {sensorId}, Value: {value}); // 处理业务逻辑... } } } }使用DeserializeAsyncEnumerable进行异步流反序列化.NET 6这是处理大型JSON数组最优雅的方式。它一次只反序列化一个数组元素到内存中。using System.Text.Json; public record DeviceReading(string SensorId, double Value); async IAsyncEnumerableDeviceReading StreamReadingsFromFileAsync(string filePath) { await using var fileStream File.OpenRead(filePath); // 反序列化一个异步的DeviceReading流 var options new JsonSerializerOptions { PropertyNameCaseInsensitive true }; await foreach (var reading in JsonSerializer.DeserializeAsyncEnumerableDeviceReading(fileStream, options)) { if (reading ! null) { yield return reading; // 每次yield一个对象内存压力极小 } } } // 消费端 await foreach (var reading in StreamReadingsFromFileAsync(huge_data.json)) { // 处理每一条数据 await ProcessReadingAsync(reading); }3.2 使用 Newtonsoft.Json 进行流式读取Newtonsoft.Json同样支持流式处理主要通过JsonTextReader类。using Newtonsoft.Json; using (var fileStream File.OpenText(large.json)) using (var jsonReader new JsonTextReader(fileStream)) { var serializer new JsonSerializer(); // 假设读取一个对象数组 if (jsonReader.Read() jsonReader.TokenType JsonToken.StartArray) { while (jsonReader.Read() jsonReader.TokenType ! JsonToken.EndArray) { // 反序列化数组中的单个对象 var reading serializer.DeserializeDeviceReading(jsonReader); ProcessReading(reading); } } }性能对比与选择在流式处理场景下System.Text.Json的Utf8JsonReader和DeserializeAsyncEnumerable在性能和内存效率上通常有显著优势特别是处理UTF-8字节源时。Newtonsoft.Json的流式API虽然功能完备但在极限性能场景下可能稍逊一筹。对于大多数应用JsonDocument和DeserializeAsyncEnumerable已经能解决99%的大文件处理问题。4. 驯服“不标准”的JSON自定义转换与复杂结构处理现实世界的JSON数据很少是教科书般的完美。你可能会遇到日期格式千奇百怪、枚举需要序列化成字符串、某些字段可能是多种类型之一多态或者JSON结构与你理想的C#模型不匹配。这就需要自定义转换器Converter出场了。4.1 处理非标准日期格式这是最常见的问题之一。API可能返回2023-10-27、27/10/2023甚至时间戳1698393600。在 System.Text.Json 中你需要编写自定义的JsonConverterT。using System.Text.Json; using System.Text.Json.Serialization; public class CustomDateTimeConverter : JsonConverterDateTime { private readonly string _format; public CustomDateTimeConverter(string format yyyy-MM-dd) { _format format; } public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType JsonTokenType.String) { if (DateTime.TryParseExact(reader.GetString(), _format, null, System.Globalization.DateTimeStyles.None, out var date)) { return date; } } else if (reader.TokenType JsonTokenType.Number) { // 处理Unix时间戳秒 return DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64()).UtcDateTime; } // 如果都不匹配可以回退到默认解析或者抛出异常 throw new JsonException($无法将值转换为DateTime。格式应为{_format}或Unix时间戳。); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { // 序列化时也按指定格式写入 writer.WriteStringValue(value.ToString(_format)); } } // 使用方式1通过特性标注在属性上 public class LogEntry { [JsonConverter(typeof(CustomDateTimeConverter))] public DateTime Timestamp { get; set; } } // 使用方式2在JsonSerializerOptions中全局添加 var options new JsonSerializerOptions(); options.Converters.Add(new CustomDateTimeConverter(dd/MM/yyyy));在 Newtonsoft.Json 中同样需要JsonConverter但API略有不同。using Newtonsoft.Json; using Newtonsoft.Json.Converters; public class UnixTimestampConverter : JsonConverterDateTime { public override DateTime ReadJson(JsonReader reader, Type objectType, DateTime existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType JsonToken.Integer) { return DateTimeOffset.FromUnixTimeSeconds((long)reader.Value).UtcDateTime; } // 处理其他格式... throw new JsonSerializationException(Expected integer token for Unix timestamp.); } public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer) { writer.WriteValue(new DateTimeOffset(value).ToUnixTimeSeconds()); } } // 使用通过 [JsonConverter] 特性 public class Event { [JsonConverter(typeof(UnixTimestampConverter))] public DateTime EventTime { get; set; } }4.2 处理多态类型一个属性可能是多种类型例如一个Data字段可能是字符串也可能是复杂对象。定义模型public class Message { public string Type { get; set; } public IDataPayload Data { get; set; } // 接口类型 } public interface IDataPayload { } public class TextPayload : IDataPayload { public string Content { get; set; } } public class ImagePayload : IDataPayload { public string Url { get; set; } public int Width { get; set; } public int Height { get; set; } }在 System.Text.Json 中实现转换器System.Text.Json对多态的支持相对较弱需要自定义转换器根据某个判别字段如Type来决定反序列化成哪种具体类型。public class DataPayloadConverter : JsonConverterIDataPayload { public override IDataPayload Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // 这里需要一个技巧因为Data是Message的一部分我们需要在Message的转换器中处理或者使用JsonDocument先读取整个对象。 // 更常见的模式是在Message的转换器中或者使用[JsonDerivedType]特性.NET 7。 throw new NotImplementedException(需要在包含Type信息的上下文中处理。); } public override void Write(Utf8JsonWriter writer, IDataPayload value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, value, value.GetType(), options); } } // .NET 7 提供了更优雅的原生支持[JsonDerivedType] [JsonDerivedType(typeof(TextPayload), typeDiscriminator: text)] [JsonDerivedType(typeof(ImagePayload), typeDiscriminator: image)] public interface IDataPayload { } // 序列化/反序列化时会自动包含一个“$type”字段来区分。在 Newtonsoft.Json 中可以使用TypeNameHandling设置但出于安全考虑通常不建议对不可信的JSON源使用。更安全的做法是自定义转换器或使用JsonConverter的Create方法。// 方法1使用 TypeNameHandling (谨慎) var settings new JsonSerializerSettings { TypeNameHandling TypeNameHandling.Auto // 自动在JSON中添加$type信息 }; // 反序列化时会根据$type创建正确类型。 // 方法2自定义转换器更安全 public class SafeDataPayloadConverter : JsonConverter { public override bool CanConvert(Type objectType) objectType typeof(IDataPayload); public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { // 假设JSON中有一个“type”字段 var jObject JObject.Load(reader); var type jObject[type]?.ToString(); IDataPayload payload type switch { text new TextPayload(), image new ImagePayload(), _ throw new JsonSerializationException($Unknown payload type: {type}) }; serializer.Populate(jObject.CreateReader(), payload); return payload; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { // 写入时也加上type字段 var type value.GetType().Name; var jObject JObject.FromObject(value); jObject.AddFirst(new JProperty(type, type.ToLowerInvariant())); jObject.WriteTo(writer); } }4.3 处理JSON结构扁平化或嵌套有时JSON的层级结构并不符合你的领域模型。例如JSON是扁平的但你想映射到一个嵌套的对象里。原始JSON{ sensorId: s1, locationBuilding: A, locationFloor: 3 }目标C#类public class Device { public string SensorId { get; set; } public Location DeviceLocation { get; set; } } public class Location { public string Building { get; set; } public string Floor { get; set; } }你可以通过自定义转换器在反序列化时从扁平JSON中构造出嵌套的Location对象。但在很多情况下更简单的做法是使用一个中间DTOData Transfer Object类来接收扁平数据然后再映射到你的领域模型。或者使用像AutoMapper这样的对象映射库。对于简单的场景System.Text.Json和Newtonsoft.Json都支持在属性上使用[JsonPropertyName]或[JsonProperty]来重命名但无法处理这种结构转换。一个折中的方案是使用JsonElement进行手动提取public class Device { public string SensorId { get; set; } [JsonIgnore] // 不直接反序列化这个属性 public Location DeviceLocation { get; set; } // 用于接收扁平字段的属性 [JsonPropertyName(locationBuilding)] public string LocationBuilding { get; set; } [JsonPropertyName(locationFloor)] public string LocationFloor { get; set; } // 在反序列化后手动或通过方法构建Location [OnDeserialized] // System.Text.Json 使用 [JsonConstructor] 或自定义转换器逻辑 internal void OnDeserializedMethod() { DeviceLocation new Location { Building LocationBuilding, Floor LocationFloor }; } }处理不标准的JSON是对开发者耐心的考验也是区分普通使用者和高级使用者的关键。核心思路是优先尝试通过配置命名策略、忽略规则解决复杂结构转换使用自定义转换器极度复杂的映射考虑引入中间DTO层。5. 实战集成在真实C#项目场景中的应用与避坑掌握了核心解析技术最终要落地到项目里。无论是你提到的上位机软件、SCPI设备控制还是Web API调用JSON解析都是不可或缺的一环。这里分享几个典型场景下的实战经验和容易踩的坑。5.1 场景一上位机软件与设备通信TCP/串口在上位机开发中经常需要通过TCP/IP或串口与PLC、传感器、仪器比如是德科技的设备常使用SCPI或自定义协议通信。现代设备越来越多地使用JSON作为数据交换格式。典型流程设备端按一定频率或触发条件将数据封装成JSON字符串发送。上位机端接收字节流需要先根据协议如以换行符\n结尾、固定长度、或包含长度头切分出完整的JSON报文。对完整的JSON字符串进行解析。关键坑点与解决方案坑点1粘包与拆包。TCP是流式协议一次Receive可能收到多条消息的一部分也可能收到多条完整消息。解决方案定义明确的报文边界。最常用的方法是使用“长度头”或“分隔符”。长度头法在JSON数据前加一个固定字节的长度字段。接收时先读长度再读取指定长度的数据。分隔符法约定以换行符(\n)或特定字符作为结束标志。使用ReadLineAsync或手动在缓冲区中查找分隔符。坑点2编码问题。确保设备发送和上位机接收使用相同的字符编码通常是UTF-8。解决方案在接收和解析时明确指定编码。// 假设从NetworkStream中读取 using var streamReader new StreamReader(networkStream, Encoding.UTF8); string jsonLine await streamReader.ReadLineAsync(); // 如果使用换行符分隔 if (!string.IsNullOrEmpty(jsonLine)) { var data JsonSerializer.DeserializeDeviceData(jsonLine); }坑点3解析性能。如果数据频率很高如毫秒级解析可能成为瓶颈。解决方案使用System.Text.Json以获得最佳性能。对于固定格式的简单JSON可以考虑使用Utf8JsonReader进行手动、零分配的解析避免创建中间对象。将解析操作放在独立的线程或Task中避免阻塞UI线程对于WinForms/WPF上位机。坑点4错误处理与重连。网络可能中断数据可能损坏。解决方案反序列化时一定要用try-catch包裹捕获JsonException。对于损坏的数据记录日志并尝试跳过或请求重发。实现心跳机制和自动重连逻辑。5.2 场景二调用RESTful API与处理响应这是最常见的场景。使用HttpClient调用API然后解析返回的JSON响应。标准做法using System.Net.Http.Json; // 这个命名空间提供了扩展方法 public class ApiService { private readonly HttpClient _httpClient; public ApiService(HttpClient httpClient) { _httpClient httpClient; } public async TaskWeatherForecast? GetWeatherAsync(string city) { try { // 使用 GetFromJsonAsync 一步到位内部使用 System.Text.Json var forecast await _httpClient.GetFromJsonAsyncWeatherForecast($/api/weather/{city}); return forecast; } catch (HttpRequestException e) { // 处理网络错误 Console.WriteLine($网络请求失败: {e.Message}); } catch (JsonException e) { // 处理JSON解析错误 Console.WriteLine($响应JSON格式错误: {e.Message}); } return null; } }进阶技巧与坑点坑点1配置JsonSerializerOptions。GetFromJsonAsync和PostAsJsonAsync等方法允许传入自定义的JsonSerializerOptions。如果你需要修改命名策略、添加转换器务必在这里传入。var options new JsonSerializerOptions { PropertyNameCaseInsensitive true, Converters { new CustomDateTimeConverter(yyyy-MM-dd HH:mm:ss) } }; var data await _httpClient.GetFromJsonAsyncMyModel(url, options);坑点2处理动态或未知结构的JSON。有时API返回的字段不确定。可以使用JsonDocument、JsonElement或Newtonsoft.Json的JObject/JToken来动态查询。using var response await _httpClient.GetAsync(url); var jsonString await response.Content.ReadAsStringAsync(); using JsonDocument doc JsonDocument.Parse(jsonString); if (doc.RootElement.TryGetProperty(data, out var dataElement) dataElement.ValueKind JsonValueKind.Array) { foreach (var item in dataElement.EnumerateArray()) { // 动态访问属性 if (item.TryGetProperty(name, out var nameProp)) { Console.WriteLine(nameProp.GetString()); } } }坑点3流式处理大型API响应。如果API返回一个巨大的JSON数组可以使用前面提到的DeserializeAsyncEnumerable。using var response await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); await using var stream await response.Content.ReadAsStreamAsync(); await foreach (var item in JsonSerializer.DeserializeAsyncEnumerableMyItem(stream)) { // 逐项处理 }5.3 场景三配置文件与本地数据存储将配置如AppSettings.json或本地缓存数据存储为JSON文件非常普遍。ASP.NET Core 配置绑定这是最经典的使用场景。框架底层使用System.Text.Json或Newtonsoft.Json取决于配置将appsettings.json反序列化到强类型的IConfiguration对象或选项类IOptions。你通常不需要手动解析。手动读写文件// 读取 string jsonText await File.ReadAllTextAsync(config.json); var config JsonSerializer.DeserializeAppConfig(jsonText); // 写入并格式化以便阅读 var options new JsonSerializerOptions { WriteIndented true }; string jsonToWrite JsonSerializer.Serialize(config, options); await File.WriteAllTextAsync(config.json, jsonToWrite);坑点文件锁与并发访问。如果多个进程或线程同时读写同一个JSON文件会导致IO异常。解决方案对于配置文件通常采用“读多写少”的模式写入时使用互斥锁或考虑使用专门的文件数据库如SQLite。对于缓存数据可以考虑使用内存缓存定期持久化的策略。5.4 通用避坑指南总结始终验证输入不要信任任何外部JSON数据。反序列化前如果可能先验证其结构或使用JsonDocument进行初步检查。处理异常务必用try-catch包裹反序列化代码捕获JsonExceptionSystem.Text.Json或JsonSerializationExceptionNewtonsoft.Json并给出有意义的错误日志。注意循环引用如果你的对象模型存在循环引用如Parent.Child-Child.ParentSystem.Text.Json默认会抛出异常。你需要配置JsonSerializerOptions.ReferenceHandler ReferenceHandler.Preserve.NET 6或使用Newtonsoft.Json并设置PreserveReferencesHandling。性能敏感处使用源生成器Source Generators.NET 6 为System.Text.Json引入了源生成器它可以在编译时生成序列化/反序列化代码完全避免运行时反射能大幅提升性能。对于高频调用的类型强烈建议使用。[JsonSerializable(typeof(MyPoco))] internal partial class MyJsonContext : JsonSerializerContext { } // 使用生成的上下文进行序列化性能极佳 var json JsonSerializer.Serialize(myPoco, MyJsonContext.Default.MyPoco);版本兼容性当你的C#模型类发生变化增删属性时考虑使用[JsonExtensionData]System.Text.Json或[JsonExtensionData]Newtonsoft.Json来存储未知的JSON属性避免反序列化失败提高接口的向前/向后兼容性。JSON解析在C#中是一个从入门到精深的技能。从简单的DeserializeObject到高性能流式处理再到处理各种边界情况每一步都对应着不同的应用场景和优化选择。希望这篇长文能帮你建立起完整的知识图谱下次再遇到JSON时能够游刃有余地选择最合适的工具和方法写出既快又稳的代码。记住没有最好的库只有最适合当前场景的库。理解原理灵活运用才是王道。