我一直觉得JSON序列化程序实际上遍历了整个对象的树,并在遇到的每个接口类型的对象上执行自定义JsonConverter的WriteJson函数-事实并非如此
我有以下类和接口:
public interface IAnimal
{
string Name { get; set; }
string Speak();
List<IAnimal> Children { get; set; }
}
public class Cat : IAnimal
{
public string Name { get; set; }
public List<IAnimal> Children { get; set; }
public Cat()
{
Children = new List<IAnimal>();
}
public Cat(string name="") : this()
{
Name = name;
}
public string Speak()
{
return "Meow";
}
}
public class Dog : IAnimal
{
public string Name { get; set; }
public List<IAnimal> Children { get; set; }
public Dog()
{
Children = new List<IAnimal>();
}
public Dog(string name="") : this()
{
Name = name;
}
public string Speak()
{
return "Arf";
}
}
为了避免JSON中的$type属性,我编写了一个自定义的JsonConverter类,其WriteJson是
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
IAnimal animal = value as IAnimal;
JObject o = (JObject)t;
if (animal != null)
{
if (animal is Dog)
{
o.AddFirst(new JProperty("type", "Dog"));
//o.Find
}
else if (animal is Cat)
{
o.AddFirst(new JProperty("type", "Cat"));
}
foreach(IAnimal childAnimal in animal.Children)
{
// ???
}
o.WriteTo(writer);
}
}
}
在这个例子中,是的,狗可以给孩子养猫,反之亦然。在转换器中,我想插入"type"属性,以便它将其保存到序列化中。我有以下设置。(动物园只有一个名字和一个动物列表。为了简洁和懒惰,我没有把它包括在这里;)
Zoo hardcodedZoo = new Zoo()
{ Name = "My Zoo",
Animals = new List<IAnimal> { new Dog("Ruff"), new Cat("Cleo"),
new Dog("Rover"){
Children = new List<IAnimal>{ new Dog("Fido"), new Dog("Fluffy")}
} }
};
JsonSerializerSettings settings = new JsonSerializerSettings(){
ContractResolver = new CamelCasePropertyNamesContractResolver() ,
Formatting = Formatting.Indented
};
settings.Converters.Add(new AnimalsConverter());
string serializedHardCodedZoo = JsonConvert.SerializeObject(hardcodedZoo, settings);
serializedHardCodedZoo
在序列化后具有以下输出:
{
"name": "My Zoo",
"animals": [
{
"type": "Dog",
"Name": "Ruff",
"Children": []
},
{
"type": "Cat",
"Name": "Cleo",
"Children": []
},
{
"type": "Dog",
"Name": "Rover",
"Children": [
{
"Name": "Fido",
"Children": []
},
{
"Name": "Fluffy",
"Children": []
}
]
}
]
}
Ruff、Cleo和Rover都有这种类型的房产,但Fido和Fluffy没有。我想WriteJson不是递归调用的。如何在那里获得该类型的属性?
顺便说一句,为什么它不像我期望的那样是骆驼式的动物呢?
转换器没有应用于子对象的原因是JToken.FromObject()
在内部使用了一个新的序列化程序实例,而该实例不知道转换器的情况。有一个重载允许您传入序列化程序,但如果您在这里这样做,您将遇到另一个问题:由于您在转换器内部,并且您正在使用JToken.FromObject()
尝试序列化父对象,因此您将进入无限递归循环。(JToken.FromObject()
调用序列化程序,它调用转换器,它调用JToken.FromObject()
,等等)
若要解决此问题,必须手动处理父对象。您可以使用一点反射来枚举父属性,而不会遇到太多麻烦:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JObject jo = new JObject();
Type type = value.GetType();
jo.Add("type", type.Name);
foreach (PropertyInfo prop in type.GetProperties())
{
if (prop.CanRead)
{
object propVal = prop.GetValue(value, null);
if (propVal != null)
{
jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
}
}
}
jo.WriteTo(writer);
}
Fiddle:https://dotnetfiddle.net/sVWsE4
这里有一个想法,不是对每个属性进行反射,而是遍历正常序列化的JObject,然后更改您感兴趣的属性的令牌。
这样,您仍然可以利用所有"JsonIgnore"属性和其他内置的有吸引力的功能。
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken jToken = JToken.FromObject(value);
if (jToken.Type == JTokenType.Object)
{
JObject jObject = (JObject)jToken;
...
AddRemoveSerializedProperties(jObject, val);
...
}
...
}
然后
private void AddRemoveSerializedProperties(JObject jObject, MahMan baseContract)
{
jObject.AddFirst(....);
foreach (KeyValuePair<string, JToken> propertyJToken in jObject)
{
if (propertyJToken.Value.Type != JTokenType.Object)
continue;
JToken nestedJObject = propertyJToken.Value;
PropertyInfo clrProperty = baseContract.GetType().GetProperty(propertyJToken.Key);
MahMan nestedObjectValue = clrProperty.GetValue(baseContract) as MahMan;
if(nestedObj != null)
AddRemoveSerializedProperties((JObject)nestedJObject, nestedObjectValue);
}
}
我在为父类型和子类型使用两个自定义转换器时遇到了这个问题。我发现的一个更简单的方法是,由于JToken.FromObject()
的重载将serializer
作为参数,因此可以传递WriteJson()
中给定的序列化程序。然而,您需要从序列化程序中删除转换器,以避免对其进行递归调用(但在之后将其添加回):
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Converters.Remove(this);
JToken jToken = JToken.FromObject(value, serializer);
serializer.Converters.Add(this);
// Perform any necessary conversions on the object returned
}
这里有一个简单的问题解决方案,可以完成工作并看起来整洁。
public class MyJsonConverter : JsonConverter
{
public const string TypePropertyName = "type";
private bool _dormant = false;
/// <summary>
/// A hack is involved:
/// " JToken.FromObject(value, serializer); " creates amn infinite loop in normal circumstances
/// for that reason before calling it "_dormant = true;" is called.
/// the result is that this JsonConverter will reply false to exactly one "CanConvert()" call.
/// this gap will allow to generate a a basic version without any extra properties, and then add them on the call with " JToken.FromObject(value, serializer); ".
/// </summary>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
_dormant = true;
JToken t = JToken.FromObject(value, serializer);
if (t.Type == JTokenType.Object && value is IContent)
{
JObject o = (JObject)t;
o.AddFirst(new JProperty(TypePropertyName, value.GetType().Name));
o.WriteTo(writer);
}
else
{
t.WriteTo(writer);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanRead => false;
public override bool CanConvert(Type objectType)
{
if (_dormant)
{
_dormant = false;
return false;
}
return true;
}
}