• C# 中使对象序列化/反序列化 Json 支持使用派生类型以及泛型的方式


    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(json).Dump();
    
    

    输出

    
    15 //b.GetValue().Dump();
    
    {} // string json = JsonSerializer.Serialize(b).Dump();
    
    Base //Base desb = JsonSerializer.Deserialize(json).Dump();
    
    

    所以我们需要做的是在序列化/反序列化的时候生成/解析它原本类型的标记信息,让我们的应用识别到他的具体类型,这样在程序中使用父类接收的地方可以保证运行时类型正确.

    System.Text.Json 提供了 JsonDerivedType 特性用以在父类中标注派生类以及序列化时候的标记名称

    
    Base b = new Sub();
    
    b.GetValue().Dump();
    
    string json = JsonSerializer.Serialize(b).Dump();
    
    Base desb = JsonSerializer.Deserialize(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(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),"subType_Int")]
        [JsonDerivedType(typeof(SubT),"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),"subType_Int")]
        public class SubT<T>:Base<T> 
        {
            public T TValue { get; set; }
      
            public override T? GetValue()
            {
                return TValue;
            }
        }
      
        [JsonDerivedType(typeof(Base),"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 desb = JsonSerializer.Deserialize>(json).Dump();BytePositionInLine: 32. 
      
      

    通过 [JsonConverter]特性 以及 [KnowType]特性标注派生类型实现支持自定义类型序列化#

    通过使用 System.Text.Json [JsonDerivedType] 可以实现简单的派生类型与基类转换.

    但是遇到复杂的派生类型例如(泛型)则显得十分无力.

    当我们需要支持复杂的类型转换的时候得需要用到另一个特性 JsonConvertAttribute 搭配自定义实现 JsonConvert 了.

    先定义一个特性用来标注序列化/反序列化过程中类型的定义包含泛型信息

    
    // 自定义泛型类型名特性
    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,Sub,由于有了泛型 可以将之前返回值从object 改为 对应的泛型T,
    并将 [GenericTypeName] 和 关键的 [JsonConverter] 添加上

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

    并实现 JsonConverter>JsonConverter>

    BaseConvert

    
    public class BaseConverter<T>:JsonConverter<Base<T>>
    {
        public override Base? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var markerAttribute = typeToConvert.GetCustomAttribute()!;
            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 PropertyName");
                    
                    // 如果名称等于标注特性上的属性名称
                    if(propertyName == genericTypeName)
                    {
                        // 提前读取
                        reader.Read();
                        typeName = reader.GetString();
                        continue;
                    }
                }else
                {
                    JsonConverter tConverter = (JsonConverter)options.GetConverter(typeof(T));
                    
                    tV = tConverter.Read(ref reader,typeof(T),options);
                }
            }
            
            ArgumentException.ThrowIfNullOrWhiteSpace(typeName);
            
            //这里只演示 ,偷懒,如果有值就为 Sub 如果要更通用的需要根据类型手动构造
            
            if(tV is not null)
            {
                return new Sub{ Value = tV };
            }
            
            return new Base();
        }
    
        public override void Write(Utf8JsonWriter writer, Base value, JsonSerializerOptions options)
        {
        
            // 获取要写入的的类型
            var sourceType = value.GetType()!;
            
            // 获取 泛型 T 类型的名称
            string gernericName = sourceType.GenericTypeArguments.First().Name;
            
            // 我们自定义的标注特性
            // 可以缓存起来
            string genericTypeName = sourceType.GetCustomAttribute()!.GenericTypePropertyName!;
            string gernericBaseTypeName = sourceType.GetCustomAttribute()!.BaseName;
            
            // 如果是派生类型的泛型
            if(sourceType.GetGenericTypeDefinition() != typeof(Base<>))
            {
                var knowTypes =  Type.GetCustomAttributes();
                
                // 从 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 本身没任何属性 写入泛型类型就结束了
            writer.WriteStartObject();
            
            writer.WriteString(JsonNamingPolicy.CamelCase.ConvertName(genericTypeName),$"{gernericBaseTypeName}_G_{gernericName}");
            
            writer.WriteEndObject();
        }
    }
    
    

    SubConverter

    
    public class SubConverter<T>: JsonConverter<Sub<T>>
    {
        public override Sub? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            // 找到用于标记的特性
            string genericTypeName = typeToConvert.GetCustomAttribute()!.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 其实就是获取 JsonConverter 而基础类型基本都内置 
                    // 所以不用专门去写 
                    JsonConverter tConverter = (JsonConverter)options.GetConverter(typeof(T));
                    
                    tV = tConverter.Read(ref reader,typeof(T),options);
                }
            }
            
            ArgumentException.ThrowIfNullOrWhiteSpace(typeName);
            
            return new Sub(){ Value = tV };
        }
    
        public override void Write(Utf8JsonWriter writer, Sub value, JsonSerializerOptions options)
        {
             var sourceType = value.GetType()!;
            
            string genericName = sourceType.GenericTypeArguments.First().Name;
            
            var markerAttribute = sourceType.GetCustomAttribute()!;
            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.Deserializeint>>(json);
    
    des.Dump();
    
    

    输出

    
    {"Value":15,"$type":"Sub_G_Int32"} // string json = JsonSerializer.Serialize(i).Dump();
    
    Sub`1[System.Int32] // des.Dump();
      Value = 15
    
    

    貌似没什么问题了...

    等等...

    泛型,那我改改类型试试

    将 上面Base,Sub 上的 JsonConvert 泛型改为 bool 试试

    输出

    
    {"Value":true,"$type":"Sub_G_Boolean"} // string json = JsonSerializer.Serialize(i).
    
    Sub`1[System.Boolean] // des.Dump();
      Value = True
    
    

    好像也没问题

    Ok, 那把 Base,Sub 上的 JsonConvertT 去掉 不指定类型 让他通用起来

    ......省略代码
    
    [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 的 , 所以我们需要将 BaseSub 上的 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 泛型序列化的过程.

    过程还是挺曲折

    感觉这个需求挺小众,找了各个网站都没有这方面的解决方案.

    不甘心的我对着微软的文档一个个特性研究,生怕错过一个关于这方面的能力.

    最后的解决方案已经满足了我的需求

    最后,上面的代码都是我想尽快发出博客手敲出来的,难免会有错误和没有达到最优性能的情况,但总体过程还是挺完整的.

  • 相关阅读:
    嵌入式Qt-交叉编译FFmpeg与视频播放测试
    Java自学(三)面向对象编程
    java python+vue运动场地租赁管理系统
    Everything——检索神兵
    Linux虚拟机的克隆
    uniapp项目实践总结(二十五)苹果 ios 平台 APP 打包教程
    C#开发的OpenRA游戏之电力系统之二
    ffmpeg抠图
    关于 java 的动态绑定机制
    CentOS7安装中文字体
  • 原文地址:https://www.cnblogs.com/ablewang/p/18068949