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
實現支援泛型序列化/反序列化
繼承並重寫 JsonConverterFactory
的 CanConvert
以及 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 泛型序列化的過程.
過程還是挺曲折
感覺這個需求挺小眾,找了各個網站都沒有這方面的解決方案.
不甘心的我對著微軟的文件一個個特性研究,生怕錯過一個關於這方面的能力.
最後的解決方案已經滿足了我的需求
最後,上面的程式碼都是我想盡快發出部落格手敲出來的,難免會有錯誤和沒有達到最優效能的情況,但總體過程還是挺完整的.