C# 中使物件序列化/反序列化 Json 支援使用派生型別以及泛型的方式

有什么不能一笑而过呢發表於2024-03-13

C# 中使物件序列化/反序列化 Json 支援使用派生型別以及泛型方式

廢話

前言

為啥想寫這個部落格

  • 最近自己寫的框架有用到這個

      類似工作流,支援節點編碼自定義,動態執行自定義.
    
      儘量減少動態解析這就需要確定型別.
    

    有什麼好的奇思妙想可以一起來討論噢 (現在還是毛坯,測試各種可能性)

  • 方便C#編碼過程有泛型 寫起來舒服

  • 編譯期確定型別

RoslynPad 以及 .Dump() 方法說明

RoslynPad 是基於Roslyn 實現的跨平臺C# 編輯器,簡潔輕巧
支援nuget引用包
支援.NET框架版本切換

.Dump() 方法是 RoslynPad 支援的一個診斷方法,方便 賦值並列印物件資訊(看作是 Console.WriteLine就行 但是 Dump方法會返回當前訪問例項 例如 int i = 1.Dump() ,i依然會被賦值為 1);

透過 [JsonDerivedType] 特性實現支援派生型別序列化/反序列化

首先定義 Base 以及 它的派生類 Sub 並重寫父類的GetValue方法


public class Sub:Base
{
    public object? Value { get; set; } = 15;
    public override object? GetValue() 
    {
        return Value;
    }
}

public class Base
{
   public virtual object? GetValue()
   {
        return default;
   }
}

當我們在程式中直接使用 Base 接收並呼叫 Sub 這個派生類的時候肯定沒有任何問題(因為b執行時型別還是原來的Sub).

但是當我們如果需要將它序列化為json字串傳輸的時候.

由於他已經脫離了原本型別的執行環境,只是一個json字串,它當中沒有任何關於它原來的型別資訊記錄,反序列化時json解析器根本不認識原來的執行時型別,他只知道應用定義的解析需要的型別是Base 而派生類 Sub.Value屬性會被丟棄,但由於程式中很多地方都是用父類型別接收的,所以會導致資訊的丟失.


using System.Text.Json;
using System.Text.Json.Serialization;

Base b = new Sub();

b.GetValue().Dump();

string json = JsonSerializer.Serialize(b).Dump();

Base desb = JsonSerializer.Deserialize<Base>(json).Dump();

輸出


15 //b.GetValue().Dump();

{} // string json = JsonSerializer.Serialize(b).Dump();

Base //Base desb = JsonSerializer.Deserialize<Base>(json).Dump();

所以我們需要做的是在序列化/反序列化的時候生成/解析它原本型別的標記資訊,讓我們的應用識別到他的具體型別,這樣在程式中使用父類接收的地方可以保證執行時型別正確.

System.Text.Json 提供了 JsonDerivedType 特性用以在父類中標註派生類以及序列化時候的標記名稱


Base b = new Sub();

b.GetValue().Dump();

string json = JsonSerializer.Serialize(b).Dump();

Base desb = JsonSerializer.Deserialize<Base>(json).Dump();


public class Sub:Base
{
    public object? Value { get; set; } = 15;
    public override object? GetValue() 
    {
        return Value;
    }
}

[JsonDerivedType(typeof(Sub),"subType")] // 新增特性
public class Base
{
   public virtual object? GetValue()
   {
        return default;
   }
}

輸出


15 //b.GetValue().Dump();

{"$type":"subType","Value":15} // string json = JsonSerializer.Serialize(b).Dump();

Sub   //Base desb = JsonSerializer.Deserialize<Base>(json).Dump();
  Value = 15
    Item = <null>
    ValueKind = Number
      value__ = 4

可以看到 b 在序列化為json字串時帶上了我們特性上指定的subType並賦值給了$type 屬性
當我們反序列化為執行時物件時應用正確反序列化為了Sub物件.

但這只是最簡單的一個場景, 我們日常使用最多的場景還是 在繼承的基礎上還要加上泛型,但System.Text.Json中預設不支援泛型的序列化/反序列化.

當我們把程式碼改造為泛型之後會得到以下錯誤

  • 無法支援泛型型別

    
    [JsonDerivedType(typeof(SubT<>),"subType_G")]
    public class Base<T>
    {
       public virtual T? GetValue()
       {
            return default;
       }
    }
    
    

    異常

    Specified type 'SubT`1[T]' is not a supported derived type for the polymorphic type 'Base`1[System.Int32]'. Derived types must be assignable to the base type, must not be generic and cannot be abstract classes or interfaces unless 'JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor' is specified.
    
  • 也無法支援不同泛型單獨定義

    
      [JsonDerivedType(typeof(SubT<int>),"subType_Int")]
      [JsonDerivedType(typeof(SubT<bool>),"subType_Bool")]
      public class Base<T>
      {
         public virtual T? GetValue()
         {
              return default;
         }
      }
    
    

    異常

    
    Specified type 'SubT`1[System.Boolean]' is not a supported derived type for the polymorphic type 'Base`1[System.Int32]'. Derived types must be assignable to the base type, must not be generic and cannot be abstract classes or interfaces unless 'JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor' is specified.
    
    
  • 當只定義單一泛型基礎型別時可以序列化,但反序列化依然異常仍需要單獨定義讀取,且父類及派生類都需要定義單一泛型型別實現定義(繁瑣且不實用,誰定義泛型只會用一種基礎型別的泛型啊)

    
      [JsonDerivedType(typeof(SubT<int>),"subType_Int")]
      public class SubT<T>:Base<T> 
      {
          public T TValue { get; set; }
    
          public override T? GetValue()
          {
              return TValue;
          }
      }
    
      [JsonDerivedType(typeof(Base<int>),"base_Int")]
      public class Base<T>
      {
         public virtual T? GetValue()
         {
              return default;
         }
      }
    
    
    15 //b.GetValue().Dump();
    
    {"$type":"subType_Int","TValue":15} // string json = JsonSerializer.Serialize(b).Dump();
    
    Read unrecognized type discriminator id 'subType_Int'. Path: $ | LineNumber: 0 |  // Base<int> desb = JsonSerializer.Deserialize<Base<int>>(json).Dump();BytePositionInLine: 32. 
    
    

透過 [JsonConverter]特性 以及 [KnowType]特性標註派生型別實現支援自定義型別序列化

透過使用 System.Text.Json [JsonDerivedType] 可以實現簡單的派生型別與基類轉換.

但是遇到複雜的派生型別例如(泛型)則顯得十分無力.

當我們需要支援複雜的型別轉換的時候得需要用到另一個特性 JsonConvertAttribute 搭配自定義實現 JsonConvert<T> 了.

先定義一個特性用來標註序列化/反序列化過程中型別的定義包含泛型資訊


// 自定義泛型型別名特性
public class GenericTypeNameAttribute:Attribute
{
    // 生成的屬性名稱
    public string GenericTypePropertyName { get; set; }
    
    // 泛型基礎名稱
    public string BaseName { get; set; }

    // 根據泛型基礎型別T屬性值
    public string GetGValue (string genericTypeName) => $"{GeneratePrefix}_{genericTypeName}";
    
    // 生成值字首
    public string GeneratePrefix => $"{BaseName}_G";
}

然後將原來的 Base ,Sub 改為 Base<T>,Sub<T>,由於有了泛型 可以將之前返回值從object 改為 對應的泛型T,
並將 [GenericTypeName] 和 關鍵的 [JsonConverter] 新增上


[GenericTypeName(GenericTypePropertyName = "$type",BaseName = nameof(Sub<T>))]
[JsonConverter(typeof(SubConverter<int>))]
public class Sub<T> :Base<T>
{
    public T Value { get; set; }
    
    public override T? GetValue() 
    {
        return Value;
    }
}

[GenericTypeName(GenericTypePropertyName = "$type",BaseName = nameof(Base<T>))]
[KnownType(typeof(Sub<>))]
[JsonConverter(typeof(BaseConverter<int>))]
public class Base<T>
{
   public virtual T? GetValue()
   {
        return default;
   }
}

並實現 JsonConverter<Base<T>>JsonConverter<Sub<T>>

BaseConvert<T>


public class BaseConverter<T>:JsonConverter<Base<T>>
{
    public override Base<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var markerAttribute = typeToConvert.GetCustomAttribute<GenericTypeNameAttribute>()!;
        string genericTypeName = markerAttribute.GenericTypePropertyName!;
        
        string? typeName = default;
        
        T? tV = default;
        
        while(reader.Read())
        {
            if(reader.TokenType == JsonTokenType.EndObject)
                break;
                
            if(reader.TokenType == JsonTokenType.PropertyName)
            {
                string propertyName = reader.GetString() ?? throw new ArgumentException("Base<T> PropertyName");
                
                // 如果名稱等於標註特性上的屬性名稱
                if(propertyName == genericTypeName)
                {
                    // 提前讀取
                    reader.Read();
                    typeName = reader.GetString();
                    continue;
                }
            }else
            {
                JsonConverter<T> tConverter = (JsonConverter<T>)options.GetConverter(typeof(T));
                
                tV = tConverter.Read(ref reader,typeof(T),options);
            }
        }
        
        ArgumentException.ThrowIfNullOrWhiteSpace(typeName);
        
        //這裡只演示 ,偷懶,如果有值就為 Sub<T> 如果要更通用的需要根據型別手動構造
        
        if(tV is not null)
        {
            return new Sub<T>{ Value = tV };
        }
        
        return new Base<T>();
    }

    public override void Write(Utf8JsonWriter writer, Base<T> value, JsonSerializerOptions options)
    {
    
        // 獲取要寫入的的型別
        var sourceType = value.GetType()!;
        
        // 獲取 泛型 T 型別的名稱
        string gernericName = sourceType.GenericTypeArguments.First().Name;
        
        // 我們自定義的標註特性
        // 可以快取起來
        string genericTypeName = sourceType.GetCustomAttribute<GenericTypeNameAttribute>()!.GenericTypePropertyName!;
        string gernericBaseTypeName = sourceType.GetCustomAttribute<GenericTypeNameAttribute>()!.BaseName;
        
        // 如果是派生型別的泛型
        if(sourceType.GetGenericTypeDefinition() != typeof(Base<>))
        {
            var knowTypes =  Type.GetCustomAttributes<KnownTypeAttribute>();
            
            // 從 KnownType 中查詢註冊型別
            var targetType = knowTypes?.FirstOrDefault(
                    x => x.Type?.GetGenericTypeDefinition() == sourceType.GetGenericTypeDefinition());
            
            if(targetType != null && targetType.Type != null)
            {
                // 構建泛型型別
                var mkType = targetType.Type.MakeGenericType(sourceType.GenericTypeArguments[0]);
                
                // 呼叫對應已註冊型別序列化方法
                writer.WriteRawValue(JsonSerializer.Serialize(value,mkType));
                return;
            }
        }
        
        // Base<T> 本身沒任何屬性 寫入泛型型別就結束了
        writer.WriteStartObject();
        
        writer.WriteString(JsonNamingPolicy.CamelCase.ConvertName(genericTypeName),$"{gernericBaseTypeName}_G_{gernericName}");
        
        writer.WriteEndObject();
    }
}

SubConverter<T>


public class SubConverter<T>: JsonConverter<Sub<T>>
{
    public override Sub<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // 找到用於標記的特性
        string genericTypeName = typeToConvert.GetCustomAttribute<GenericTypeNameAttribute>()!.GenericTypePropertyName!;
        
        // 並未用到這個typeName 只是用來記錄 可以根據具體需求使用
        string? typeName = default;
        
        T tV = default;
        
        // 當可以繼續讀取的時候
        while(reader.Read())
        {
            // 讀到json物件末尾了 退出
            if(reader.TokenType == JsonTokenType.EndObject)
                break;
            
            // 讀到屬性名稱記錄下
            if(reader.TokenType == JsonTokenType.PropertyName)
            {
                string propertyName = reader.GetString();
                
                // 如果屬性名稱是特性標記中的名稱
                if(propertyName == genericTypeName)
                {
                    // 手動繼續讀取
                    reader.Read();
                    // 獲取到名稱
                    typeName = reader.GetString();
                    // 並跳過當此迴圈 因為以及預讀取過
                    continue;
                }
            }else
            {
                // 當初也在想怎麼構建 泛型 T 的型別的例項
                // 後面參照官網示例
                // 是透過獲取 T 對應的 JsonConverter 獲取 並呼叫 Read 方法構建 (妙啊)
                // 例如: T為 int 則 JsonConverter<T> 其實就是獲取 JsonConverter<int> 而基礎型別基本都內建 
                // 所以不用專門去寫 
                JsonConverter<T> tConverter = (JsonConverter<T>)options.GetConverter(typeof(T));
                
                tV = tConverter.Read(ref reader,typeof(T),options);
            }
        }
        
        ArgumentException.ThrowIfNullOrWhiteSpace(typeName);
        
        return new Sub<T>(){ Value = tV };
    }

    public override void Write(Utf8JsonWriter writer, Sub<T> value, JsonSerializerOptions options)
    {
         var sourceType = value.GetType()!;
        
        string genericName = sourceType.GenericTypeArguments.First().Name;
        
        var markerAttribute = sourceType.GetCustomAttribute<GenericTypeNameAttribute>()!;
        string genericTypePropName = markerAttribute.GenericTypePropertyName!;
        
        writer.WriteStartObject();
        
        if(value is Sub<string> st)
        {
            writer.WriteString("Value",st.Value);
        }else if(value is Sub<int> it)
        {
            writer.WriteNumber("Value",it.Value);
        }else if(value is Sub<bool> bt)
        {
            writer.WriteBoolean("Value",bt.Value);
        }
        
        writer.WriteString(JsonNamingPolicy.CamelCase.ConvertName(genericTypePropName),markerAttribute.GetGValue(genericName));
        writer.WriteEndObject();
    }
}

完成上述步驟之後我們就可以愉快的開始愉快的泛型序列化了......嗎?

將我們的呼叫改為泛型呼叫


Base<int> i = new Sub<int>{ Value = 15 };

string json = JsonSerializer.Serialize(i).Dump();

Base<int> des = JsonSerializer.Deserialize<Base<int>>(json);

des.Dump();

輸出


{"Value":15,"$type":"Sub_G_Int32"} // string json = JsonSerializer.Serialize(i).Dump();

Sub`1[System.Int32] // des.Dump();
  Value = 15

貌似沒什麼問題了...

等等...

泛型,那我改改型別試試

將 上面Base<T>,Sub<T> 上的 JsonConvert<T> 泛型改為 bool 試試

輸出


{"Value":true,"$type":"Sub_G_Boolean"} // string json = JsonSerializer.Serialize(i).

Sub`1[System.Boolean] // des.Dump();
  Value = True

好像也沒問題

Ok, 那把 Base<T>,Sub<T> 上的 JsonConvert<T>T 去掉 不指定型別 讓他通用起來

......省略程式碼

[JsonConverter(typeof(BaseConverter<>))]
public class Base<T>

......省略程式碼

執行試試


Cannot create an instance of BaseConverter`1[T] because Type.ContainsGenericParameters is true.

啊 ?

竟然異常了,這不是玩我嗎 ? 竟然 JsonConvertAttribute 傳入的 Type 不支援泛型
從異常資訊來看 ,好像是某種約束預設不讓泛型引數

because Type.ContainsGenericParameters is true

經過一番查詢最後在微軟官方指引裡發現了 JsonConverterFactory 這個類,用來
給支援泛型的房子加上最後一塊磚

藉由 JsonConverterFactory 實現支援泛型序列化/反序列化

繼承並重寫 JsonConverterFactoryCanConvert 以及 CreateConverter 方法


// 定義泛型轉換器建立工廠
public abstract class GenericTypeConverterFactory : JsonConverterFactory
{
    // 泛型型別
    public abstract Type GenericType { get; }
    
    // 對應轉換器泛型型別
    public abstract Type GenericJsonConvertType { get; }
    
    // 什麼型別可以轉換
    public override bool CanConvert(Type typeToConvert)
    {
        // 這裡約束了只有泛型型別可以轉換
        return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == GenericType;
    }

    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        // 獲取泛型型別
        Type valueType = typeToConvert.GetGenericArguments()[0];

        // 手動構造泛型轉換器的型別
        Type converterType = GenericJsonConvertType.MakeGenericType(valueType);
        
        // 獲取對應的例項
        var ist = (JsonConverter?)Activator.CreateInstance(converterType);
        
        return ist;
    }
}

public sealed class BaseConverterFactory:GenericTypeConverterFactory
{
    public override Type GenericType => typeof(Base<>);
    public override Type GenericJsonConvertType => typeof(MyConverter<>);
}

public sealed class SubConverterFactory:GenericTypeConverterFactory
{
    public override Type GenericType => typeof(Sub<>);
    public override Type GenericJsonConvertType => typeof(SubConverter<>);
}

由於 JsonConverterFactory 是繼承 JsonConverter 的 , 所以我們需要將 Base<T>Sub<T> 上的 JsonConvert 替換為剛剛實現的兩個工廠

......省略程式碼

[JsonConverter(typeof(BaseConverterFactory))]
public class Base<T>

......省略程式碼

執行 bool


{"Value":true,"$type":"Sub_G_Boolean"}

Sub`1[System.Boolean]
  Value = True


執行 int


{"Value":12,"$type":"Sub_G_Int32"}

Sub`1[System.Int32]
  Value = 12


執行 string


{"Value":"hello world","$type":"Sub_G_String"}

Sub`1[System.String]
  Value = hello world


完美 !!!!

結尾

上面就是我探索 json 泛型序列化的過程.

過程還是挺曲折

感覺這個需求挺小眾,找了各個網站都沒有這方面的解決方案.

不甘心的我對著微軟的文件一個個特性研究,生怕錯過一個關於這方面的能力.

最後的解決方案已經滿足了我的需求

最後,上面的程式碼都是我想盡快發出部落格手敲出來的,難免會有錯誤和沒有達到最優效能的情況,但總體過程還是挺完整的.

相關文章