Bootstrap

LitJson序列化反序列化支持多态(正确反序列化子类)

Json序列化反序列化支持多态

本文章的Json序列化使用LitJson库,当然使用其他库基本思路也是一样的。

1、LitJson多态序列化反序列化现在的不足

基础的序列化和反序列化测试代码如下:

using LitJson;
using System;

[Serializable]
public class ClassBaseA
{
    public int a;
}

[Serializable]
public class ClassExtendB : ClassBaseA
{
    public int b;
}

[Serializable]
public class SerializeClass
{
    public int normalField = 1;
    public ClassBaseA specialFiled;
}

class Program
{
    public static void Main(string[] args)
    {
        var b = new ClassExtendB() {a = 1, b = 2};
        SerializeClass toJsonObject = new SerializeClass() {specialFiled = b};
        
        // 序列化
        var jsonString = LitJson.JsonMapper.ToJson(toJsonObject);
        Console.WriteLine(jsonString);

        // 反序列化
        SerializeClass fromJsonObject = LitJson.JsonMapper.ToObject<SerializeClass>(jsonString);
        Console.WriteLine(LitJson.JsonMapper.ToJson(fromJsonObject));
    }
}

得到的结果为:

{“normalField”:1,“specialFiled”:{“b”:2,“a”:1}}
{“normalField”:1,“specialFiled”:{“a”:1}}

我们的字段 specialFiled.b 丢失了,这是由于类 SerializeClass 里面定义的字段 specialFiled 类型为 ClassBaseA ,所以对应的字段 b 被忽略了。根本原因是 LitJson 是使用反射来获取类中的字段类型,并且发现序列化Json字符串中含有未知字段会直接跳过,具体可见 JsonMapper.cs 源码。

1、序列化

使用LitJson库进行Json的序列化,默认支持多态的,所以导出部分看起来是正常的。

2、反序列化

LitJson反序列化是使用反射实现的,代码可以参考 JsonMapper.cs 的函数

private static object ReadValue(Type inst_type, JsonReader reader, bool skipFirstRead = false)

基本思路就是反射获取类里面的 Field 和 Property ,根据Json字符串里面键和字段名进行匹配,然后再把Json字符串里面的值转换为字段对应类型的数据。

3、修改思路

其中的问题在,反序列化是通过反射直接获取的,因此传入的子类Json数据(示例里的ClassExtendB)里只有原始类(示例里的SerializeClass)定义时的类(示例里的ClassBaseA)里面的 Field 和 Property 才能正确序列化,其余数据都会被丢弃。所以,我们只要把反序列化时反射获取定义的类(ClassBaseA)变成获取实际类(ClassExtendB)就可以了。

4、解决方案

  1. 多态标签
    定义一个 Attribute 标明是不是要对该类应用多态序列化和反序列化

    namespace LitJson
    {
        using System;
    
        /// <summary>
        /// 该类序列化与反序列化是否多态
        /// </summary>
        [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)]
        public class PolymorphismJsonAttribute : Attribute
        {
    
        }
    }
    

    感谢评论中的朋友提醒,修改成可以判断接口是否含有 Attribute,函数定义为:

    // 判断某个类、接口是否有 PolymorphismJsonAttribute 属性
    private static bool TypeIsPolymorphismJsonAttribute(Type t)
    {
        bool hasAttribute = System.Attribute.GetCustomAttribute(t, typeof(PolymorphismJsonAttribute), true) != null;
        if (hasAttribute) return true;
            
        foreach (var interfaceType in t.GetInterfaces())
        {
            if (System.Attribute.GetCustomAttribute(interfaceType, typeof(PolymorphismJsonAttribute), true) != null)
            {
                hasAttribute = true;
                break;
            }
        }
    
        return hasAttribute;
    }
    
  2. 序列化时添加类信息
    在 JsonMapper.cs 的 WriteValue 函数里面,序列化一个Object时,如果有 PolymorphismJsonAttribute 就把该类的实际程序集名和类名记录下来,这样就可以在反序列化时使用了。我加的代码在两段 add code for PolymorphismJsonAttribute 内,其余部分为 LitJson 源码。

    // LitJson something else
    writer.WriteObjectStart();
    
    // add code for PolymorphismJsonAttribute begin
    // 打了多态序列化标签的话,就记录对应数据
    if (TypeIsPolymorphismJsonAttribute(obj_type))
    {
        writer.WritePropertyName("__ASSEMBLY__");
        writer.Write(obj_type.Assembly.FullName);
        writer.WritePropertyName("__TYPE__");
        writer.Write(obj_type.FullName);
    }
    // add code for PolymorphismJsonAttribute end
    
    // LitJson something else (wrirte object real)
    writer.WriteObjectEnd();
    // LitJson something else
    
  3. 反序列化时根据类信息创建对象
    在 JsonMapper.cs 的 ReadValue 函数里面, 反序列化一个 object,如果有 PolymorphismJsonAttribute 这个标签就用对应的数据(__ASSEMBLY____TYPE__)来确定实际的类。

    // ReadValue 函数定义有修改,添加一个参数标记要不要跳过第一个读取操作
    private static object ReadValue (Type inst_type, JsonReader reader, bool skipFirstRead = false)
    {
        if (!skipFirstRead)
        {
            reader.Read ();   
        }
    
    	// LitJson something else
    	
    	while (true)
    	{
    	// LitJson something else (Continuously read and break)
    	   if (t_data.Properties.ContainsKey(property))
    	   {
    	       PropertyMetadata prop_data = t_data.Properties[property];
    	       if (prop_data.IsField)
    	       {
    	           // add code for PolymorphismJsonAttribute begin
    	           // 这里是直接获取字段的 prop_data.Type, 但是对于需要使用多态的序列化而言,是有一定问题的
    	           // 所以加个属性PolymorphismJsonAttribute,如果属性有定义,说明需要使用多态来进行处理
    	           if (TypeIsPolymorphismJsonAttribute(prop_data.Type))
    	           {
    	               reader.Read(); // ObjectStart
    	               reader.Read(); // __ASSEMBLY__
    	               reader.Read();
    	               string assembly = (string)reader.Value;
    	               reader.Read(); // __TYPE__
    	               reader.Read();
    	               string type = (string)reader.Value;
    	               Type t = Assembly.Load(assembly).GetType(type);
    	               reader.Token = JsonToken.ObjectStart;
    	               ((FieldInfo)prop_data.Info).SetValue(instance, ReadValue(t, reader, true)); // 这里第三个参数用来标记跳过读取操作
    	           }
    	           else
    	           {
    	               ((FieldInfo)prop_data.Info).SetValue(instance, ReadValue(prop_data.Type, reader));
    	           }
    	           // add code for PolymorphismJsonAttribute end
    	       }
    	       else
    	       {
    			// LitJson something else
    	       }
    	   }
    	   // LitJson something else
    	}
    	// LitJson something else
    }
    

5、结果

只要像以上稍微改动一点,我们就可以正确显示结果了。在 ClassBaseA 上加上我们新定义的标签 PolymorphismJson,其余保持不变。

[LitJson.PolymorphismJson]
public class ClassBaseA
{
    public int a;
}

结果为:

{“normalField”:1,“specialFiled”:{"__ASSEMBLY__":“ConsoleApplication1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null”,"__TYPE__":“ClassExtendB”,“b”:2,“a”:1}}
{“normalField”:1,“specialFiled”:{"__ASSEMBLY__":“ConsoleApplication1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null”,"__TYPE__":“ClassExtendB”,“b”:2,“a”:1}}

结果多记录了类的程序集名和类名,我们想要的字段b保留了。

代码详情参见: https://github.com/kimomi/LitJsonPolymorphism

;