手把手教你用System.Text.Json搞定C#里那些‘不听话’的JSON数据(含自定义转换器实战)
手把手教你用System.Text.Json搞定C#里那些‘不听话’的JSON数据(含自定义转换器实战)
在对接第三方API时,开发者经常会遇到JSON数据结构与C#实体类不匹配的棘手问题。比如API返回的字段类型与实体类定义不符,或者JSON中存在动态属性、嵌套层级过深等情况。这些问题如果处理不当,轻则导致反序列化失败,重则引发生产环境的数据解析异常。
System.Text.Json作为.NET Core 3.0引入的官方JSON库,相比Newtonsoft.Json在性能上有显著优势,但在处理"不标准"的JSON数据时,需要掌握一些特殊技巧。本文将从一个真实API对接案例出发,演示如何用System.Text.Json的各种高级功能驯服这些"不听话"的JSON数据。
1. 理解System.Text.Json的核心机制
System.Text.Json采用了一种基于Utf8JsonReader/Writer的低分配设计,这是其性能优势的关键。与Newtonsoft.Json不同,它在默认情况下执行严格的类型检查,这既是优点也是需要特别注意的地方。
典型问题场景示例:
{ "userId": "12345", "price": "99.99", "metadata": { "createdAt": "2023-07-20T12:00:00Z", "tags": ["urgent", "vip"] } }对应的C#实体类:
public class Order { public int UserId { get; set; } // JSON中是字符串 public decimal Price { get; set; } // JSON中是字符串 public Metadata Meta { get; set; } } public class Metadata { public DateTime CreatedAt { get; set; } public List<string> Tags { get; set; } }直接使用JsonSerializer.Deserialize<Order>(jsonString)会抛出异常,因为类型不匹配。这就是我们需要解决的问题。
2. 基础解决方案:JsonPropertyName与JsonIgnore
对于简单的字段名不匹配问题,可以使用[JsonPropertyName]特性:
public class Order { [JsonPropertyName("userId")] public int UserId { get; set; } [JsonIgnore] public decimal OriginalPrice { get; set; } [JsonPropertyName("price")] public string PriceString { get; set; } [JsonIgnore] public decimal Price => decimal.Parse(PriceString); }这种方法适用于:
- JSON字段名与C#属性名不一致
- 需要忽略某些属性
- 需要进行简单的格式转换
局限性:
- 无法处理复杂的类型转换
- 需要手动处理null值
- 代码会变得冗长
3. 高级技巧:自定义JsonConverter实战
对于更复杂的场景,我们需要创建自定义转换器。以下是一个处理字符串与数值类型互转的通用转换器:
public class FlexibleNumberConverter : JsonConverter<object> { public override bool CanConvert(Type typeToConvert) { return typeToConvert == typeof(int) || typeToConvert == typeof(long) || typeToConvert == typeof(decimal) || typeToConvert == typeof(double); } public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { var str = reader.GetString(); if (string.IsNullOrEmpty(str)) return null; return typeToConvert switch { Type t when t == typeof(int) => int.Parse(str), Type t when t == typeof(long) => long.Parse(str), Type t when t == typeof(decimal) => decimal.Parse(str), Type t when t == typeof(double) => double.Parse(str), _ => throw new JsonException() }; } else if (reader.TokenType == JsonTokenType.Number) { return typeToConvert switch { Type t when t == typeof(int) => reader.GetInt32(), Type t when t == typeof(long) => reader.GetInt64(), Type t when t == typeof(decimal) => reader.GetDecimal(), Type t when t == typeof(double) => reader.GetDouble(), _ => throw new JsonException() }; } throw new JsonException(); } public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); } }使用方法:
var options = new JsonSerializerOptions { Converters = { new FlexibleNumberConverter() } }; var order = JsonSerializer.Deserialize<Order>(jsonString, options);4. 处理动态属性和复杂嵌套结构
对于包含动态属性或深度嵌套的JSON,可以使用JsonDocument或JsonElement进行灵活处理:
public class DynamicOrder { [JsonExtensionData] public Dictionary<string, JsonElement> ExtensionData { get; set; } public T GetDynamicValue<T>(string propertyName) { if (ExtensionData.TryGetValue(propertyName, out var element)) { return element.ValueKind switch { JsonValueKind.String => (T)(object)element.GetString(), JsonValueKind.Number => (T)(object)element.GetDecimal(), JsonValueKind.True => (T)(object)true, JsonValueKind.False => (T)(object)false, _ => default }; } return default; } }使用场景对比表:
| 场景 | 推荐方案 | 优点 | 缺点 |
|---|---|---|---|
| 简单字段映射 | JsonPropertyName | 简单直接 | 功能有限 |
| 复杂类型转换 | 自定义JsonConverter | 灵活强大 | 实现复杂 |
| 动态属性 | JsonExtensionData | 处理未知字段 | 需要手动解析 |
| 临时处理 | JsonDocument | 完全控制 | 代码量大 |
5. 性能优化与最佳实践
System.Text.Json的性能优势体现在多个方面:
- 重用JsonSerializerOptions:
// 错误做法:每次创建新options var order = JsonSerializer.Deserialize<Order>(jsonString, new JsonSerializerOptions()); // 正确做法:重用options实例 private static readonly JsonSerializerOptions _options = new() { PropertyNameCaseInsensitive = true, Converters = { new FlexibleNumberConverter() } };- 使用源生成器(.NET 6+):
[JsonSerializable(typeof(Order))] public partial class OrderContext : JsonSerializerContext {} // 使用生成的序列化代码 var order = JsonSerializer.Deserialize(jsonString, OrderContext.Default.Order);- 异步流处理:
await using var stream = File.OpenRead("large.json"); var orders = await JsonSerializer.DeserializeAsync<List<Order>>(stream, _options);性能对比数据:
- 小型对象(<1KB):System.Text.Json快2-3倍
- 大型对象(>100KB):快1.5-2倍
- 连续处理1000个对象:内存占用减少30%
在实际项目中,根据JSON数据的复杂度和性能要求选择合适的处理方式。对于简单的DTO,直接使用属性标记即可;对于复杂的业务对象,自定义转换器是更好的选择;而当需要处理未知结构时,JsonElement提供了最大的灵活性。
