网上有很多关于Newtonsoft.Json循环引用的解决方案,比如设置循环引用为Ignore,这样在输出JSON时就不会输出。
var setting = new JsonSerializerSettings();
setting.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
var json = JsonConvert.SerializeObject(data, setting);
但是这个结果不是我想要的。我想要的是循环引用的数据得保留,以便后面反序列化时能还原数据,所以将循环引用设置为序列化。
setting.ReferenceLoopHandling = ReferenceLoopHandling.Serialize
这样设置后,在将对象序列化成JSON时却报错了,查了很多资料,做了很多尝试,于是找到了一种解决办法,只要加一个PreserveReferencesHandling的设置,代码如下
var data = GenerateData();
var setting = new JsonSerializerSettings();
setting.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
setting.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
var json = JsonConvert.SerializeObject(data, setting);
序列化的结果如下
成功序列化成了JSON数据,注意观察数据会发现多了$id的引用。成功之后,迫不急待的将JSON反序列化成对象,要记得使用序列化时同样的setting,否则会出错。
var setting = new JsonSerializerSettings();
setting.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
setting.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
var node = JsonConvert.DeserializeObject<Node>(json, setting);
经过这样的设置后,基本上循环引用的序列化和反序列化算是解决了。但是把这个方式应用到项目的时候,又遇到坑了。首先就是接口的坑,因为反序列化时要执行构造函数,接口是没有构造函数的,而我们在做类的属性的时候常常会用到接口类型,那这样怎么整?又是查资料测试,后面找到了答案,需要增加如下设置
setting.TypeNameHandling = TypeNameHandling.All;
TypeNameHandling说是可以设置为Auto和All,设置为Auto则会自动判断,但可能不可靠,所以设置为All比较可靠。设置后的内容如下
{
"$id": "1",
"$type": "JsonConvertTest.Node, JsonConvertTest",
"Name": "根",
"Parent": null,
"Childs": {
"$type": "System.Collections.Generic.List`1[[JsonConvertTest.Node, JsonConvertTest]], mscorlib",
"$values": [
{
"$id": "2",
"$type": "JsonConvertTest.Node, JsonConvertTest",
"Name": "子1",
"Parent": { "$ref": "1" },
"Childs": null,
"Description": null
},
{
"$id": "3",
"$type": "JsonConvertTest.Node, JsonConvertTest",
"Name": "子2",
"Parent": { "$ref": "1" },
"Childs": null,
"Description": null
},
{
"$id": "4",
"$type": "JsonConvertTest.Node, JsonConvertTest",
"Name": "子3",
"Parent": { "$ref": "1" },
"Childs": null,
"Description": null
}
]
},
"Description": {
"$id": "5",
"$type": "JsonConvertTest.Description, JsonConvertTest",
"Content": "测试内容",
"Author": "张三"
}
}
从结果中会看到多了$type的字段,该字段包含了具体的类型,这样在反序列化的时候就会依据该类型找到对应的类,然后执行反序化。有人会问了,为什么有了类型后接口就可以反序列化了?因为接口只是定义了类型,但具体存什么数据是代码运行得到的,存数据时虽然这是接口,但是存的是数据的具体类型,接口是没办法直接作为数据的,必须是实现接口的类,既然是类,那就必定会有构造函数的。
对于接口的坑,抽象类也会有相似的坑。
接口的坑填上了,原以为已经圆满解决了,谁知道有些不需要序列化的数据也执行了序列化,原因是JSON序列化时会自动把公有的属性、字段这些都执行序列化,但是有的数据是不能序列化的,比如程序内部运行时的控件类型。那要怎么办?
一般的解决方案是把不要序列化的属性标识为JsonIgnore,如果是项目初期是可行的,可是项目已经进展到一定程度,要把每个属性都标识一遍是不太可能了。经分析后,发现因为另一种保存数据的需求而在项目中对要保存的属性和字段作了一个Attribute的标识,那就想这个标识能不能用起来呢?答案是可以的。
这时我们要用到IContractResolver接口,直接实现这个接口有难度,我们可以继承已经实现该接口的DefaultContractResolver类,然后对该类中的一些方法进行重写。这里我们要重写的是CreateProperty方法,代码如下
public class MyContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var saveDataAttribute = member.GetCustomAttribute<SaveDataAttribute>();
if (saveDataAttribute == null)
{
return null;
}
else
{
return base.CreateProperty(member, memberSerialization);
}
}
}
其中SaveDataAttribute是继承自Attribute的类,没有任何代码,只是继承了而已,主要是做标识用的。
public class SaveDataAttribute : Attribute
{
}
这样在标识有SaveDataAttribute属性的字段就会被输出到json中。要序列化的类如下。
[SaveData]
public class Node
{
/// <summary>
/// 名称
/// </summary>
[SaveData]
public string Name { get;private set; }
/// <summary>
/// 父
/// </summary>
[SaveData]
public Node Parent { get; set; }
/// <summary>
/// 子
/// </summary>
[SaveData]
public List<Node> Childs { get; set; }
/// <summary>
/// 描述
/// </summary>
public IData Description { get; set; }
/// <summary>
/// 内部数据
/// </summary>
public string InnerData { get; set; }
}
其中Description和InnerData没有被标识为SaveData,所以序列化的时候这两个字段并没有序列化,结果如下
{
"$id": "1",
"$type": "JsonConvertTest.Node, JsonConvertTest",
"Name": "根",
"Parent": null,
"Childs": {
"$type": "System.Collections.Generic.List`1[[JsonConvertTest.Node, JsonConvertTest]], mscorlib",
"$values": [
{
"$id": "2",
"$type": "JsonConvertTest.Node, JsonConvertTest",
"Name": "子1",
"Parent": { "$ref": "1" },
"Childs": null
},
{
"$id": "3",
"$type": "JsonConvertTest.Node, JsonConvertTest",
"Name": "子2",
"Parent": { "$ref": "1" },
"Childs": null
},
{
"$id": "4",
"$type": "JsonConvertTest.Node, JsonConvertTest",
"Name": "子3",
"Parent": { "$ref": "1" },
"Childs": null
}
]
}
}
经过这样的处理后,数据是正常序列化成JSON了,但是反序列化成对象的时候,发现有的数据为空,就像上面的Name的数据总是为空,追查原因,发现是因为属性的set方法标识成了private,所以导致反序列化的时候没有写入数据,那要怎么办呢?这个就要看具体的业务需求,我们是不管标识成private还是其他的,反序列化时一定要还原数据来,为此,我们需要让json库还原的时候知道这个字段是要可写的,于是我们回到了刚刚的CreateProperty方法,将其Writable、Readable都设置为true,代码如下
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var saveDataAttribute = member.GetCustomAttribute<SaveDataAttribute>();
if (saveDataAttribute == null)
{
return null;
}
else
{
var property= base.CreateProperty(member, memberSerialization);
property.Writable = true;
property.Readable = true;
return property;
}
}
这样设置后,数据就正常还原了。但是在还原的时候又碰到了新的问题,有的类还是不能正常还原,会报构造函数错误。经查,是因为没有空参数的构造函数。这时需要将构造函数标识为JsonConstructorAttribute,这样JSON库在反序列化的时候就会调用该构造函数。
[JsonConstructor]
public Node(string name)
{
this.Name = name;
}
但是这样处理后,在项目的应用中还是有问题,因为有好多含参的构造函数都有业务逻辑在里面,在反序列化时逻辑执行会出问题,从而导致还原数据失败。即便是无参构造函数,内部也有的有些逻辑,所以很不好处理。经分析后,发现原来为了另一种需求去保存数据时,也是有还原数据的需求,为了不参与原有的逻辑,只是纯粹的还原数据,于是对类作了标识,同时增加了特殊的构造函数,该构造函数有一个特殊的参数,于是还原数据时,执行该构造函数去创建实例。有了这样的思路,我们就考虑在JSON库中要怎么用起来。查资料后,得知继承DefaultContractResolver的类中通过重写CreateObjectContract方法可以实现,代码如下
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var jsonObjectContract = base.CreateObjectContract(objectType);
var saveDataAttribute = objectType.GetCustomAttributes(typeof(SaveDataAttribute), true);
if (saveDataAttribute != null && saveDataAttribute.Length > 0)
{
jsonObjectContract.DefaultCreator = () => Activator.CreateInstance(jsonObjectContract.CreatedType, new DataRestoreObjectType());
}
return jsonObjectContract;
}
数据类的特殊构造函数
public Node(DataRestoreObjectType dataRestoreObjectType)
{
}
经过以上的处理后,序列化和反序化JSON数据的循环引用问题成功得到解决,同时也与原有的项目很好的兼容在一起。
转载请注明出处。