Skip to content

JSON 序列化:最常见的问题所在

在 .NET AOT (Ahead-of-Time) 编译模式下,JSON 处理是开发者最容易遇到的“陷阱”。问题的根源在于,常规的 JSON 序列化操作在默认情况下严重依赖于运行时的反射 (Reflection)

问题根源:反射与代码裁剪 (Trimming) 的冲突

当你调用标准的 System.Text.Json.JsonSerializer.Serialize(object)Deserialize<T>(jsonString) 方法时,JsonSerializer 在内部会执行以下操作:

  1. 类型发现:通过反射检查对象 T 的类型。
  2. 成员扫描:查找并获取 T 类型的所有公共属性 (properties) 和字段 (fields)。
  3. 值操作:动态地调用这些属性的 getset 方法来读取或写入数据。

然而,AOT 编译的一个关键步骤是 裁剪 (Trimming)。编译器会分析你的代码,移除所有它认为“未被直接引用”的部分,以最大程度地减小最终生成的可执行文件体积。

这就导致了一个致命的冲突: 如果你代码中只通过泛型参数 T 的形式使用了某个类,比如 JsonSerializer.Deserialize<MyPoco>(json),AOT 编译器无法在编译时“看到”任何直接调用 MyPoco 构造函数或其属性的代码。因此,它会做出一个“合理”的推断:MyPoco 的构造函数和属性都是无用代码,并将它们从最终的程序中裁剪掉

结果就是在运行时,JsonSerializer 尝试通过反射去寻找那些已经被移除的构造函数和属性,最终导致:

  • 反序列化失败:可能抛出异常,或者更隐蔽地返回一个所有属性都为 null 或默认值的空对象。
  • 序列化失败:可能生成一个空的 JSON 对象 {},因为找不到任何可供序列化的属性。

✅ 解决方案:拥抱源码生成器 (Source Generator)

为了从根本上解决这一问题,.NET 团队提供了官方的解决方案:JSON 源码生成器 (JSON Source Generator)

它的核心思想非常清晰:将原本在运行时通过反射完成的工作,全部提前到编译时来完成。

源码生成器就像一个编译插件,它会在编译期间:

  1. 扫描你的代码,找到你明确标记需要序列化的类型。
  2. 为这些类型自动生成高效、无反射的序列化和反序列化逻辑代码
  3. 将这些生成的 C# 代码与你的项目代码一起编译成本机代码。

这样一来,运行时就不再需要任何反射,因为所有关于如何处理特定类型(如 MyPoco)的指令都已经是静态的、预先编译好的代码了。

如何正确操作:JsonSerializerContext 指南

在 AOT 项目中,正确处理 JSON 的步骤如下:

  1. 创建 JsonSerializerContext 你需要定义一个 partial 类,让它继承自 System.Text.Json.Serialization.JsonSerializerContext。然后,使用 [JsonSerializable] 特性来“注册”所有你计划要序列化或反序列化的数据模型。

    csharp
    using System.Text.Json.Serialization;
    
    namespace MyAotApp;
    
    // 1. [必需] 定义一个 partial 类继承 JsonSerializerContext。
    // 2. [必需] 为每一个需要序列化的 POCO (Plain Old C# Object) 添加 [JsonSerializable] 特性。
    [JsonSerializable(typeof(WeatherForecast))]
    [JsonSerializable(typeof(UserSession))]
    [JsonSerializable(typeof(ApiErrorResponse))]
    // ...
    internal partial class AppJsonSerializerContext : JsonSerializerContext
    {
        // 这个类是空的,它的另一部分代码将由 Source Generator 在编译时自动创建。
    }
    • partial 关键字:这是必需的,它允许源码生成器在另一个自动生成的文件中为 AppJsonSerializerContext “补充”上序列化逻辑代码。
    • [JsonSerializable] 特性:这是你给编译器的明确指令,告诉它:“请为 WeatherForecast 这个类型保留所有成员,并为它生成序列化代码。”
  2. 调用 JsonSerializer 时传入 Context 在进行序列化或反序列化时,必须调用那些接受 JsonSerializerContextJsonTypeInfo<T> 参数的 JsonSerializer 重载方法。

    csharp
    var jsonString = "{\"date\":\"2024-01-01T00:00:00\",\"temperatureC\":25}";
    var myForecast = new WeatherForecast { /* ... */ };
    
    // --- 错误的方式 (在AOT中会失败) ---
    // var forecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString);
    // var json = JsonSerializer.Serialize(myForecast);
    
    // --- ✅ 正确的方式 (使用源码生成器,AOT安全) ---
    
    // 反序列化
    var forecast = JsonSerializer.Deserialize(
        jsonString,
        AppJsonSerializerContext.Default.WeatherForecast // 传入生成的类型信息
    );
    
    // 序列化
    var json = JsonSerializer.Serialize(
        myForecast,
        AppJsonSerializerContext.Default.WeatherForecast // 同样传入类型信息
    );
    
    // 对于需要指定类型的泛型方法,也可以这样做:
    var forecast2 = JsonSerializer.Deserialize<WeatherForecast>(
        jsonString,
        AppJsonSerializerContext.Default // 传入Context实例
    );
    var json2 = JsonSerializer.Serialize(
        myForecast,
        AppJsonSerializerContext.Default // 传入Context实例
    );