1. 为什么我们需要关注JsonSerializer
在C#开发中,数据序列化就像是我们日常生活中的"打包"过程。想象一下你要搬家,需要把各种形状大小不一的物品装进标准尺寸的纸箱里——这就是序列化在做的事情。而JsonSerializer就是.NET提供的一个专业"打包师傅",它能将内存中的对象转换成JSON格式的字符串(序列化),也能把JSON字符串还原成对象(反序列化)。
最近几年,随着微服务架构和前后端分离的普及,JSON已成为事实上的数据交换标准。但你可能不知道的是,根据2023年的开发者调查报告,超过78%的.NET项目都在使用JsonSerializer进行数据序列化操作。这个数字背后反映的是JsonSerializer的几个关键优势:
- 性能表现:相比传统的DataContractJsonSerializer,System.Text.Json中的JsonSerializer在基准测试中显示出2-3倍的性能提升
- 内存效率:新的API设计减少了中间对象的创建,降低了GC压力
- 安全性:默认配置下能有效防范常见的反序列化攻击(这点我们稍后会详细讨论)
提示:虽然Newtonsoft.Json(Json.NET)仍然是很多项目的选择,但从.NET Core 3.0开始内置的System.Text.Json.JsonSerializer正在成为微软官方推荐的标准方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JsonSerializer基础用法详解
2.1 基本序列化操作
让我们从一个最简单的例子开始。假设我们有一个表示用户的类:
csharp复制public class User
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime RegisterDate { get; set; }
}
要将User对象序列化为JSON字符串,只需要一行代码:
csharp复制var user = new User { Id = 1, Name = "张三", RegisterDate = DateTime.Now };
string json = JsonSerializer.Serialize(user);
得到的json字符串会是这样的:
json复制{"Id":1,"Name":"张三","RegisterDate":"2023-07-20T14:30:22.1234567+08:00"}
2.2 基本反序列化操作
反过来,如果我们有一个JSON字符串想转回User对象:
csharp复制string json = "{\"Id\":1,\"Name\":\"李四\",\"RegisterDate\":\"2023-07-20T00:00:00\"}";
User user = JsonSerializer.Deserialize<User>(json);
这里有几个需要注意的点:
- JSON中的属性名必须与类中的属性名完全匹配(默认区分大小写)
- 日期时间格式需要符合ISO 8601标准
- 如果JSON字符串格式不正确,会抛出JsonException
2.3 常用配置选项
JsonSerializer提供了丰富的配置选项,通过JsonSerializerOptions类来设置:
csharp复制var options = new JsonSerializerOptions
{
WriteIndented = true, // 美化输出,带缩进
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // 属性名转为驼峰命名
IgnoreNullValues = true, // 忽略null值
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping // 放松字符转义规则
};
string json = JsonSerializer.Serialize(user, options);
配置后的输出会变成:
json复制{
"id": 1,
"name": "张三",
"registerDate": "2023-07-20T14:30:22.1234567+08:00"
}
3. 高级特性与自定义控制
3.1 处理特殊数据类型
某些数据类型需要特别注意:
日期时间处理
csharp复制var options = new JsonSerializerOptions
{
Converters = { new DateTimeConverterUsingDateTimeParse() } // 自定义日期格式转换
};
枚举类型处理
默认情况下,枚举会被序列化为数字。如果想序列化为字符串:
csharp复制options.ConvertEnumToStrings = true;
循环引用处理
当对象之间存在循环引用时:
csharp复制options.ReferenceHandler = ReferenceHandler.Preserve;
3.2 自定义序列化行为
通过特性标注可以精细控制序列化过程:
csharp复制public class Product
{
[JsonPropertyName("product_id")] // 自定义JSON属性名
public int Id { get; set; }
[JsonIgnore] // 忽略此属性
public string InternalCode { get; set; }
[JsonInclude] // 包含非公共成员
private string SecretKey = "ABC123";
}
更复杂的场景可以实现自定义转换器:
csharp复制public class CustomDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.Parse(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("yyyy-MM-dd"));
}
}
使用自定义转换器:
csharp复制options.Converters.Add(new CustomDateTimeConverter());
4. 性能优化与最佳实践
4.1 源生成器(Source Generator)
.NET 6引入的源生成器可以显著提升序列化性能:
csharp复制[JsonSerializable(typeof(User))]
public partial class UserContext : JsonSerializerContext {}
// 使用生成的序列化代码
string json = JsonSerializer.Serialize(user, UserContext.Default.User);
这种方式避免了运行时反射,性能可提升30-50%。
4.2 缓冲池使用
对于高频调用的场景,可以使用ArrayBufferWriter来减少内存分配:
csharp复制var buffer = new ArrayBufferWriter<byte>();
using (var writer = new Utf8JsonWriter(buffer))
{
JsonSerializer.Serialize(writer, user);
}
// 获取UTF8字节
ReadOnlySpan<byte> jsonUtf8 = buffer.WrittenSpan;
4.3 异步序列化
处理大对象或流数据时,使用异步方法:
csharp复制await using var stream = new MemoryStream();
await JsonSerializer.SerializeAsync(stream, user);
5. 安全考量与常见问题
5.1 反序列化安全问题
虽然System.Text.Json在设计上比一些第三方库更安全,但仍需注意:
- 类型安全:避免直接反序列化到object或dynamic类型
- 深度限制:设置最大深度防止栈溢出
csharp复制options.MaxDepth = 64; - 未知属性:忽略JSON中的额外属性
csharp复制
options.UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip;
5.2 常见错误处理
日期格式问题
csharp复制try
{
var user = JsonSerializer.Deserialize<User>(json);
}
catch (JsonException ex) when (ex.Message.Contains("The JSON value could not be converted to System.DateTime"))
{
// 处理日期格式错误
}
大小写敏感问题
csharp复制options.PropertyNameCaseInsensitive = true; // 忽略属性名大小写
缺失属性处理
csharp复制[JsonRequired] // 标记属性为必需
public string Name { get; set; }
6. 实战案例:API开发中的应用
6.1 ASP.NET Core中的集成
在Startup.cs中配置:
csharp复制services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.WriteIndented = true;
});
6.2 自定义错误响应
csharp复制[ApiController]
public class ApiControllerBase : ControllerBase
{
protected IActionResult JsonResult(object data)
{
return new JsonResult(data, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
}
6.3 性能敏感场景的优化
对于高频API,使用源生成器模式:
csharp复制[HttpGet]
public IActionResult GetUser(int id)
{
var user = _userService.GetUser(id);
return Content(JsonSerializer.Serialize(user, UserContext.Default.User), "application/json");
}
7. 与Newtonsoft.Json的对比与迁移
7.1 主要差异点
| 特性 | System.Text.Json | Newtonsoft.Json |
|---|---|---|
| 性能 | 更高 | 较低 |
| 内存使用 | 更少 | 较多 |
| 功能丰富度 | 基本功能 | 非常丰富 |
| 默认安全性 | 更高 | 需手动配置 |
| 源生成器支持 | 有 | 无 |
7.2 迁移指南
- 全局替换命名空间:
using Newtonsoft.Json;→using System.Text.Json;
- 特性替换:
[JsonProperty]→[JsonPropertyName][JsonIgnore]保持不变
- 方法替换:
JsonConvert.SerializeObject→JsonSerializer.SerializeJsonConvert.DeserializeObject→JsonSerializer.Deserialize
7.3 兼容性处理
如果需要同时支持两者,可以创建适配器:
csharp复制public static class JsonHelper
{
public static string Serialize<T>(T obj, bool useNewtonsoft = false)
{
return useNewtonsoft
? Newtonsoft.Json.JsonConvert.SerializeObject(obj)
: System.Text.Json.JsonSerializer.Serialize(obj);
}
}
8. 调试技巧与工具推荐
8.1 调试序列化问题
- 使用Visual Studio的调试工具查看JsonSerializer的内部状态
- 对于复杂对象,可以先序列化为JsonNode进行中间处理:
csharp复制var node = JsonSerializer.Deserialize<JsonNode>(json);
// 调试修改node
string modifiedJson = node.ToJsonString();
8.2 性能分析工具
- 使用Benchmark.NET进行性能测试
- 使用dotMemory分析内存使用情况
8.3 实用扩展方法
csharp复制public static class JsonExtensions
{
public static string ToJson<T>(this T obj, JsonSerializerOptions options = null)
{
return JsonSerializer.Serialize(obj, options);
}
public static T FromJson<T>(this string json, JsonSerializerOptions options = null)
{
return JsonSerializer.Deserialize<T>(json, options);
}
}
使用方式:
csharp复制var json = user.ToJson();
var newUser = json.FromJson<User>();
9. 实际项目中的经验分享
在大型电商系统中使用JsonSerializer时,我们总结出以下几点经验:
- 保持一致性:在整个项目中统一序列化配置,避免不同地方使用不同设置
- 版本兼容:为API响应添加版本信息,便于后续格式变更
csharp复制public class ApiResponse<T> { public string Version { get; set; } = "1.0"; public T Data { get; set; } } - 日志优化:序列化大对象时,先进行筛选再记录日志
- 文化差异:明确指定文化信息,特别是日期和数字格式
csharp复制var options = new JsonSerializerOptions { NumberHandling = JsonNumberHandling.AllowReadingFromString, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
10. 未来发展与替代方案
虽然System.Text.Json已经成为.NET生态中的主流选择,但仍有其他替代方案值得了解:
- MessagePack:二进制格式,性能更高
- Protobuf:Google的二进制序列化格式
- MemoryPack:新兴的零分配序列化方案
选择序列化方案时的考虑因素:
- 性能需求
- 跨平台兼容性
- 可读性要求
- 安全性需求
在最近的一个高性能服务项目中,我们最终选择了JsonSerializer而非二进制方案,主要基于以下考虑:
- 调试方便性(JSON可读)
- 与前端交互的直接性
- 足够满足性能指标
- 官方长期支持的可靠性
