ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C# JSON解析全攻略:从Newtonsoft.Json到System.Text.Json性能实战

C# JSON解析全攻略:从Newtonsoft.Json到System.Text.Json性能实战

1. 项目概述:为什么C#开发者必须精通JSON解析

在今天的软件开发里,JSON(JavaScript Object Notation)几乎成了数据交换的“普通话”。无论是调用一个Web API、读取配置文件,还是在不同服务间传递消息,你大概率会碰到它。作为一名C#开发者,如果你还在用字符串拼接和截取的方式来处理JSON,那就像是在高速公路上骑自行车——不是不行,但效率低且危险。我见过不少项目,因为JSON解析不当,导致数据错乱、性能瓶颈,甚至安全漏洞。所以,花时间把JSON解析吃透,绝对不是浪费时间,而是提升你开发效率和代码质量的必经之路。

C#社区为处理JSON提供了非常强大的工具链,从经典的Newtonsoft.Json到如今官方力推的System.Text.Json,选择很多,但坑也不少。这篇文章,我会结合我十多年的C#开发经验,从最基础的序列化反序列化,到高性能场景下的流式处理、自定义转换器,再到那些官方文档里不会写的“坑”和实战技巧,为你彻底拆解C#解析JSON的方方面面。无论你是刚接触C#的新手,还是想优化现有代码的老鸟,都能在这里找到直接能用的“干货”。

2. 核心工具选型:Newtonsoft.Json 与 System.Text.Json 的深度对比

当你准备在C#项目中处理JSON时,第一个灵魂拷问就是:用哪个库?这直接决定了你后续开发的体验和代码的性能。目前主流的选择有两个:老牌劲旅Newtonsoft.Json(又称Json.NET)和微软官方的后起之秀System.Text.Json。很多新手会直接问“哪个更好?”,但我的经验是:没有绝对的好坏,只有是否适合你的场景。

2.1 Newtonsoft.Json:功能全面的“瑞士军刀”

Newtonsoft.Json由 James Newton-King 创建,在过去十多年里一直是C#社区处理JSON的事实标准。它的最大优点是功能极其丰富,几乎你能想到的所有JSON操作场景,它都有对应的解决方案。

它的核心优势在于灵活性和容错性:

  • 高度可配置的序列化行为:你可以通过JsonSerializerSettings精细控制几乎所有细节,比如如何处理空值、日期格式、循环引用、命名策略(驼峰、帕斯卡)等。例如,你想让所有属性名在序列化时自动转为小写蛇形命名(snake_case),几行配置就能搞定。
  • 强大的LINQ to JSON(JToken体系):除了简单的对象映射,它还提供了JObjectJArrayJToken等动态类型。当你需要在不定义强类型类的情况下,动态查询、修改或创建JSON结构时,这个功能是无价之宝。比如解析一个结构不确定的第三方API响应,提取其中某个深层嵌套的字段。
  • 出色的容错能力:对于格式不太严格或包含额外字段的JSON,Newtonsoft.Json通常能更“宽容”地处理,而不会直接抛出异常。

一个典型的Newtonsoft.Json反序列化示例:

using Newtonsoft.Json; public class Person { public string Name { get; set; } public int Age { get; set; } public DateTime Birthday { get; set; } } string json = @"{""name"": ""张三"", ""age"": 30, ""birthday"": ""1993-05-15""}"; // 配置序列化设置:日期格式和命名策略 var settings = new JsonSerializerSettings { DateFormatString = "yyyy-MM-dd", // ContractResolver 等可以用于更复杂的命名映射 }; Person person = JsonConvert.DeserializeObject<Person>(json, settings); Console.WriteLine(person.Name); // 输出:张三

注意:Newtonsoft.Json默认使用反射来获取和设置属性,这在大量、高频操作的场景下可能会成为性能瓶颈。虽然它功能强大,但“重量”也相对较大。

2.2 System.Text.Json:追求性能的“官方新锐”

从.NET Core 3.0开始,微软推出了System.Text.Json作为内置库,其设计初衷就是高性能和低内存分配。如果你的项目目标是高吞吐量、低延迟(例如微服务、Web API后端),那么它往往是更好的选择。

它的核心优势在于性能和安全性:

  • 原生高性能:它利用Span<T>Utf8JsonReader/Writer等现代.NET特性,在序列化和反序列化时速度更快,内存开销更小。根据官方基准测试,其性能通常数倍于Newtonsoft.Json
  • 默认安全:为了性能和安全性,它做了一些“不那么宽容”的设计。例如,属性名称默认区分大小写,并且默认不允许尾随逗号。这迫使开发者写出更规范的JSON,减少了潜在的错误。
  • 异步序列化支持:原生支持SerializeAsyncDeserializeAsync,对于处理大文件或网络流非常友好。
  • 源码生成器(Source Generator):这是它的“大杀器”。通过源码生成,可以在编译时而非运行时生成序列化/反序列化代码,彻底消除反射开销,性能可以达到极致。对于性能敏感的热点路径,这是必选项。

一个典型的System.Text.Json反序列化示例:

using System.Text.Json; public class Person { // 使用特性进行精细控制 [JsonPropertyName("full_name")] // 映射JSON中的不同字段名 public string Name { get; set; } public int Age { get; set; } [JsonConverter(typeof(DateTimeConverter))] // 使用自定义转换器 public DateTime Birthday { get; set; } } string json = @"{""full_name"": ""李四"", ""age"": 25, ""birthday"": ""1998-08-20""}"; // 配置选项 var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, // 启用不区分大小写 WriteIndented = true // 美化输出(格式化) }; Person person = JsonSerializer.Deserialize<Person>(json, options); Console.WriteLine(person.Name); // 输出:李四

2.3 选型决策指南:我该如何选择?

为了避免你纠结,我根据自己的经验总结了一个决策表:

特性/场景Newtonsoft.JsonSystem.Text.Json我的建议
新项目(.NET 5+)-优先使用 System.Text.Json。它是框架的一部分,无需额外依赖,且为未来性能优化铺平道路。
遗留项目或大量依赖-如果项目已深度集成 Newtonsoft.Json,且无显著性能问题,不建议盲目迁移,成本可能很高。
需要极致性能-✅(尤其用源码生成器)必须使用 System.Text.Json + 源码生成器。对于高频调用的API、游戏、金融交易等场景,这是唯一选择。
处理不规则/动态JSON✅(JObject好用)⚠️(可用JsonNode,但稍弱)如果需要频繁动态操作JSON结构,Newtonsoft.Json 的 LINQ to JSON 更顺手
需要高度自定义序列化✅(极其灵活)✅(足够,但略繁琐)两者都能满足绝大多数需求。Newtonsoft.Json 的配置方式有时更直观。
目标框架是 .NET Framework⚠️(需要额外安装)老项目用 Newtonsoft.Json。.NET Framework 项目虽可安装 System.Text.Json 包,但体验非原生。

实操心得:在实际项目中,我经常采用“混合策略”。主体架构和新模块使用System.Text.Json以保证性能和现代性。但对于那些需要处理非常动态、结构不定的第三方数据接口的特定模块,我会单独引入Newtonsoft.Json来处理,避免把核心代码搞复杂。只需注意控制好两者的使用边界,不要混用同一个对象的序列化/反序列化即可。

3. 基础到精通:System.Text.Json 核心操作全解析

选定System.Text.Json作为主力后,我们来深入它的核心用法。掌握这些,你就能应对90%的日常开发场景。

3.1 序列化与反序列化:对象与JSON的互转

这是最基础也是最常用的操作。JsonSerializer.SerializeJsonSerializer.Deserialize是你的主要工具。

基础用法:

var product = new Product { Id = 1, Name = "笔记本电脑", Price = 5999.99M, InStock = true }; string jsonString = JsonSerializer.Serialize(product); // 输出:{"Id":1,"Name":"笔记本电脑","Price":5999.99,"InStock":true} Product deserializedProduct = JsonSerializer.Deserialize<Product>(jsonString);

通过JsonSerializerOptions控制行为:这个选项对象是你定制序列化过程的关键。常用的配置有:

var options = new JsonSerializerOptions { WriteIndented = true, // 美化输出,带缩进和换行,用于调试 PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // 属性名转为驼峰命名 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 忽略null值 Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping // 放宽字符转义(如中文) }; string json = JsonSerializer.Serialize(product, options);

注意:PropertyNamingPolicy只影响序列化输出时的属性名。反序列化时,默认是区分大小写进行匹配的。如果需要反序列化时也忽略大小写,必须显式设置PropertyNameCaseInsensitive = true。这是新手常踩的坑。

3.2 处理特殊数据类型:日期、枚举、集合

JSON标准本身不支持日期和枚举等类型,所以如何处理它们需要额外配置。

  • 日期时间:默认序列化为 ISO 8601 格式(如"2023-10-27T14:48:00Z")。你可以通过JsonSerializerOptions.Converters添加自定义转换器,或者使用内置的JsonConverter
    options.Converters.Add(new JsonStringEnumConverter()); // 枚举序列化为字符串 // 自定义日期格式 options.Converters.Add(new DateTimeConverterUsingDateTimeParse()); // 需自己实现
  • 枚举:默认序列化为数字。使用JsonStringEnumConverter可以将其序列化为字符串,可读性更好,也便于与其他系统交互。
  • 集合与字典:List<T>Dictionary<string, T>等都能被很好地支持。字典的Key如果是复杂类型,需要特别注意。

3.3 使用特性进行精细控制

通过在模型类上添加特性(Attribute),你可以实现更精细的字段级控制,而无需全局配置。

public class Order { [JsonPropertyName("order_id")] // 序列化后字段名为 order_id public int Id { get; set; } [JsonIgnore] // 完全忽略此属性,不参与序列化和反序列化 public string InternalCode { get; set; } [JsonInclude] // 即使是非公共属性(如私有setter),也包含在序列化中 public string CustomerNote { get; private set; } [JsonConverter(typeof(CustomDecimalConverter))] // 为该属性指定自定义转换器 public decimal TotalAmount { get; set; } [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] // 允许数字从字符串读取 public int Quantity { get; set; } }

实操心得:我倾向于将全局配置(JsonSerializerOptions)用于项目级的统一约定(如驼峰命名、忽略Null值),而将特性用于处理个别字段的特殊情况。这样代码的意图更清晰,也避免了全局配置被过度修改导致难以维护。

4. 高级应用与性能优化实战

当你掌握了基础,就可以挑战更复杂的场景和追求极致的性能了。这部分内容能让你从“会用”升级到“精通”。

4.1 动态JSON处理:JsonDocument 与 JsonNode

不是所有JSON都能对应到预定义的C#类。对于需要查询、修改或动态构建的JSON,JsonDocumentJsonNode是你的利器。

  • JsonDocument:提供只读、高性能的文档对象模型(DOM)。它使用Utf8JsonReader在后台解析,内存效率极高,适合仅需查询一次或几次的大型JSON。

    string json = @"{ ""name"": ""数据中心"", ""servers"": [ {""id"": 1, ""status"": ""online""}, {""id"": 2, ""status"": ""offline""} ] }"; using JsonDocument document = JsonDocument.Parse(json); JsonElement root = document.RootElement; string name = root.GetProperty("name").GetString(); foreach (JsonElement server in root.GetProperty("servers").EnumerateArray()) { int id = server.GetProperty("id").GetInt32(); // ... 处理服务器信息 } // 使用 using 确保及时释放资源

    重要提示:JsonDocument和从它获取的JsonElementref struct,它们的数据直接指向原始JSON数据的缓冲区,因此不能存储在类的字段中,也不能在异步方法中跨越 await 使用。它的设计就是为了高性能的单次遍历。

  • JsonNode(.NET 6+):提供可变的DOM,类似于Newtonsoft.JsonJToken。你可以方便地添加、修改和删除节点。

    JsonNode rootNode = JsonNode.Parse(json)!; // 修改值 rootNode["name"] = "新数据中心"; // 添加新节点 rootNode["location"] = "上海"; // 获取新的JSON字符串 string newJson = rootNode.ToJsonString();

    JsonNode用起来更灵活,但性能开销比JsonDocument大。选择原则是:只读查询用JsonDocument,需要修改则用JsonNode

4.2 自定义JsonConverter:处理“奇葩”数据格式

当你遇到无法用内置规则序列化的类型时,就需要自定义JsonConverter<T>。常见场景包括:

  • 将特定的字符串格式(如"1,2,3,4")反序列化为List<int>
  • 处理多态类型(一个属性可能是A类,也可能是B类)。
  • 自定义日期格式(如Unix时间戳)。

示例:实现一个将Point结构体序列化为"x,y"字符串的转换器。

public struct Point { public int X; public int Y; } public class PointConverter : JsonConverter<Point> { public override Point Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // 预期JSON格式:"10,20" string pointString = reader.GetString()!; var parts = pointString.Split(','); return new Point { X = int.Parse(parts[0]), Y = int.Parse(parts[1]) }; } public override void Write(Utf8JsonWriter writer, Point value, JsonSerializerOptions options) { writer.WriteStringValue($"{value.X},{value.Y}"); } } // 使用 var options = new JsonSerializerOptions(); options.Converters.Add(new PointConverter()); Point p = new Point { X = 10, Y = 20 }; string json = JsonSerializer.Serialize(p, options); // 输出:"10,20" Point p2 = JsonSerializer.Deserialize<Point>("\"5,15\"", options); // 从字符串反序列化

实操心得:编写自定义转换器时,务必处理好边界情况,如空值、格式错误等。在Read方法中,要充分利用Utf8JsonReader提供的方法(TryGet...)来安全地读取数据,而不是直接Get...然后期待它一定正确。

4.3 性能核武器:源码生成器(Source Generator)

这是System.Text.Json在性能上的终极解决方案。它通过在编译时分析你的类型,直接生成高效的序列化代码,完全避免了运行时的反射和动态代码生成。

如何使用:

  1. 在项目中安装System.Text.Json包(通常已包含)。
  2. 创建一个分部类(partial class),并为其添加[JsonSerializable]特性。
  3. 在项目文件中启用源码生成。

示例:

// 你的模型 public class HighPerfModel { public int Id { get; set; } public string Data { get; set; } } // 在另一个文件(如 HighPerfModel.JsonContext.cs)中定义上下文 [JsonSerializable(typeof(HighPerfModel))] [JsonSerializable(typeof(List<HighPerfModel>))] public partial class AppJsonContext : JsonSerializerContext { }

在项目文件 (.csproj) 中,确保有:

<PropertyGroup> <LangVersion>latest</LangVersion> </PropertyGroup>

现在,你可以使用生成的上下文进行序列化,性能极高:

var model = new HighPerfModel { Id = 1, Data = "test" }; // 使用生成的序列化器 byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(model, AppJsonContext.Default.HighPerfModel); HighPerfModel deserialized = JsonSerializer.Deserialize(jsonBytes, AppJsonContext.Default.HighPerfModel)!;

性能对比实测:在我参与的一个高频交易数据推送服务中,将核心模型的序列化从反射模式切换到源码生成模式后,JSON处理部分的CPU耗时下降了约65%,GC压力也显著减少。对于热点路径,这绝对是值得投入的优化。

5. 实战避坑指南与疑难问题排查

理论讲得再多,不如踩几个坑来得实在。下面是我在多年开发中积累的一些常见问题和解决方案,希望能帮你少走弯路。

5.1 循环引用与对象引用处理

当你序列化的对象图中存在循环引用(例如,Order对象包含Customer,而Customer又有一个Orders列表指向回原来的Order),默认的序列化器会陷入无限循环并最终抛出JsonException

在 System.Text.Json 中处理:System.Text.Json默认不处理循环引用,这是出于性能和语义清晰度的考虑。你有几种选择:

  1. 修改数据模型:这是最推荐的方式。使用DTO(数据传输对象),在序列化时只包含必要的、非循环的数据。例如,Customer的序列化视图里不包含Orders列表。
  2. 使用[JsonIgnore]特性:在会引起循环的属性上标记忽略。
  3. 使用ReferenceHandler.Preserve(.NET 6+):这会在JSON中插入$id$ref元数据来保留引用信息。
    var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, WriteIndented = true };
    注意:这样生成的JSON不是标准JSON,可能与其他不支持此元数据的系统不兼容。

与 Newtonsoft.Json 的差异:Newtonsoft.Json可以通过ReferenceLoopHandling.Ignore简单地忽略循环引用,或者用Preserve模式。在迁移旧代码时,如果发现循环引用问题,需要仔细审查数据模型。

5.2 多态类型反序列化(一个接口,多种实现)

这是另一个经典难题。JSON中有一个Type字段,根据它的值,需要反序列化成不同的具体类。

[ { "Type": "Circle", "Radius": 5 }, { "Type": "Rectangle", "Width": 10, "Height": 20 } ]

在 System.Text.Json 中的解决方案:你需要编写一个自定义的JsonConverter。基本思路是在Write方法中写入一个类型鉴别器(如Type字段),在Read方法中读取这个鉴别器,然后决定反序列化成哪种具体类型。这需要一些样板代码,社区也有不少辅助库可以简化这个过程。

一个简化的模式:

[JsonConverter(typeof(ShapeConverter))] public abstract class Shape { public abstract string Type { get; } } public class Circle : Shape { public override string Type => "Circle"; public double Radius { get; set; } } public class ShapeConverter : JsonConverter<Shape> { public override Shape Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using var doc = JsonDocument.ParseValue(ref reader); var root = doc.RootElement; var type = root.GetProperty("Type").GetString(); return type switch { "Circle" => root.Deserialize<Circle>(options), _ => throw new JsonException($"未知的形状类型: {type}") }; } // Write 方法也需要相应实现 }

5.3 常见异常与调试技巧

  • JsonException: The JSON value could not be converted to...

    • 原因:最常见。JSON中的数据类型与C#属性类型不匹配(如JSON是字符串,但C#属性是int)。
    • 排查:仔细对比JSON字符串和你的模型类。使用JsonSerializerOptions.PropertyNameCaseInsensitive = true可以解决大小写不匹配问题。使用[JsonNumberHandling]特性可以处理数字以字符串形式出现的情况。
  • 性能问题:在循环中频繁创建JsonSerializerOptions

    • 原因:JsonSerializerOptions在首次使用时需要初始化,创建成本较高。
    • 解决:务必将其缓存起来,作为静态单例使用。这是提升性能最简单有效的方法之一。
    public static class JsonDefaults { public static readonly JsonSerializerOptions DefaultOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase // ... 其他配置 }; }
  • 内存泄漏(与 JsonDocument 相关):忘记释放JsonDocument

    • 原因:JsonDocument实现了IDisposable。如果解析大量JSON而不释放,会导致内存堆积。
    • 解决:始终使用using语句包裹JsonDocument的使用,或者确保在不再需要时调用Dispose()
  • 调试技巧:当反序列化失败时,异常信息可能不够详细。一个有用的方法是,先尝试用JsonDocument.ParseJsonNode.Parse解析JSON,看看结构是否如你所想。或者,在序列化时使用WriteIndented = true输出格式化的JSON,便于肉眼检查。

6. 综合实战:构建一个健壮的配置读取器

让我们用一个完整的、贴近实际的例子来串联以上所有知识点:编写一个从JSON文件读取应用配置的类。这个配置可能很复杂,包含嵌套对象、数组、枚举,并且我们需要它高性能、易扩展。

目标:读取如下appsettings.json

{ "AppName": "订单处理服务", "LogLevel": "Information", "Database": { "ConnectionString": "Server=.;Database=OrderDb;", "TimeoutSeconds": 30 }, "Endpoints": [ { "Name": "API", "Url": "https://api.example.com", "Protocol": "Https" }, { "Name": "Webhook", "Url": "http://localhost:8080", "Protocol": "Http" } ], "Features": { "EnableCache": true, "CacheDurationMinutes": 5 } }

步骤1:定义强类型配置模型

public class AppConfig { public string AppName { get; set; } = string.Empty; public LogLevel LogLevel { get; set; } // 枚举类型 public DatabaseConfig Database { get; set; } = new(); public List<EndpointConfig> Endpoints { get; set; } = new(); public FeatureFlags Features { get; set; } = new(); } public class DatabaseConfig { public string ConnectionString { get; set; } = string.Empty; public int TimeoutSeconds { get; set; } } public class EndpointConfig { public string Name { get; set; } = string.Empty; public string Url { get; set; } = string.Empty; public Protocol Protocol { get; set; } } public enum Protocol { Http, Https } public enum LogLevel { Debug, Information, Warning, Error } public class FeatureFlags { public bool EnableCache { get; set; } public int CacheDurationMinutes { get; set; } }

步骤2:实现高性能配置读取器我们使用源码生成器来获得最佳性能,并异步读取文件。

[JsonSerializable(typeof(AppConfig))] public partial class AppConfigJsonContext : JsonSerializerContext { } public static class ConfigurationLoader { // 缓存配置选项和上下文 private static readonly JsonSerializerOptions s_options = new() { PropertyNameCaseInsensitive = true, WriteIndented = true // 仅调试用,生产环境可关闭 }; public static async Task<AppConfig> LoadConfigAsync(string filePath) { if (!File.Exists(filePath)) throw new FileNotFoundException($"配置文件未找到: {filePath}"); await using FileStream fileStream = File.OpenRead(filePath); // 使用源码生成的反序列化方法,性能最优 AppConfig? config = await JsonSerializer.DeserializeAsync( fileStream, AppConfigJsonContext.Default.AppConfig // 使用生成的上下文 ); return config ?? throw new InvalidOperationException("反序列化配置失败,结果为null。"); } // 备用方法:使用反射(无需源码生成上下文) public static async Task<AppConfig> LoadConfigReflectionAsync(string filePath) { await using FileStream fileStream = File.OpenRead(filePath); AppConfig? config = await JsonSerializer.DeserializeAsync<AppConfig>(fileStream, s_options); return config ?? throw new InvalidOperationException("反序列化配置失败。"); } }

步骤3:使用配置

// 在Program.cs或启动类中 var config = await ConfigurationLoader.LoadConfigAsync("appsettings.json"); Console.WriteLine($"应用名称: {config.AppName}"); Console.WriteLine($"数据库超时: {config.Database.TimeoutSeconds}秒"); foreach (var endpoint in config.Endpoints) { Console.WriteLine($"端点: {endpoint.Name} - {endpoint.Url}"); }

这个实战案例涵盖的关键点:

  1. 模型设计:使用嵌套类清晰地对齐JSON结构。
  2. 枚举处理:默认序列化为字符串,可读性好。
  3. 异步操作:使用DeserializeAsync处理文件流,避免阻塞。
  4. 性能优化:使用源码生成器上下文 (AppConfigJsonContext) 进行反序列化,这是生产环境的最佳实践。
  5. 错误处理:检查文件存在性,处理反序列化结果为null的情况。
  6. 配置复用:JsonSerializerOptions缓存为静态字段,避免重复创建开销。

通过这样一个完整的例子,你应该能体会到,将System.Text.Json的各项特性组合起来,可以构建出既健壮又高性能的解决方案。关键在于根据场景选择正确的工具和方法,而不是死记硬背API。JSON解析本身不复杂,但把它用对、用好,却是区分普通开发者和资深开发者的一个细节。

返回列表