Json.NET,如何自定义序列化以插入JSON属性

我一直无法找到JsonConvert.WriteJson的合理实现,它允许我在序列化特定类型时插入JSON属性。 我的所有尝试都导致“JsonSerializationException:使用类型XXX检测到自引用循环”。

关于我正在尝试解决的问题的更多背景:我使用JSON作为配置文件格式,并且我使用JsonConverter来控制我的配置类型的类型解析,序列化和反序列化。 我不想使用$type属性,而是想使用更有意义的JSON值来解析正确的类型。

在我的简化示例中,这里有一些JSON文本:

 { "Target": "B", "Id": "foo" } 

其中JSON属性"Target": "B"用于确定此对象应序列化为B 。 考虑到这个简单的例子,这种设计似乎并不那么引人注目,但它确实使配置文件格式更加有用。

我还希望配置文件是可循环访问的。 我有反序列化的工作,我无法工作的是序列化案例。

我的问题的根源是我找不到使用标准JSON序列化逻辑的JsonConverter.WriteJson的实现,并且不会抛出“自引用循环”exception。 这是我的实现:

 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JProperty typeHintProperty = TypeHintPropertyForType(value.GetType()); //BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''. // Same error occurs whether I use the serializer parameter or a separate serializer. JObject jo = JObject.FromObject(value, serializer); if (typeHintProperty != null) { jo.AddFirst(typeHintProperty); } writer.WriteToken(jo.CreateReader()); } 

在我看来,这是Json.NET中的一个错误,因为应该有办法做到这一点。 不幸的是,我遇到的所有JsonConverter.WriteJson示例(例如JSON.NET中特定对象的自定义转换 )仅提供特定类的自定义序列化,使用JsonWriter方法写出单个对象和属性。

这是展示我的问题的xunit测试的完整代码(或在此处查看 )

 using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using Xunit; public class A { public string Id { get; set; } public A Child { get; set; } } public class B : A {} public class C : A {} ///  /// Shows the problem I'm having serializing classes with Json. ///  public sealed class JsonTypeConverterProblem { [Fact] public void ShowSerializationBug() { A a = new B() { Id = "foo", Child = new C() { Id = "bar" } }; JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); jsonSettings.ContractResolver = new TypeHintContractResolver(); string json = JsonConvert.SerializeObject(a, Formatting.Indented, jsonSettings); Console.WriteLine(json); Assert.Contains(@"""Target"": ""B""", json); Assert.Contains(@"""Is"": ""C""", json); } [Fact] public void DeserializationWorks() { string json = @"{ ""Target"": ""B"", ""Id"": ""foo"", ""Child"": { ""Is"": ""C"", ""Id"": ""bar"", } }"; JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); jsonSettings.ContractResolver = new TypeHintContractResolver(); A a = JsonConvert.DeserializeObject(json, jsonSettings); Assert.IsType(a); Assert.IsType(a.Child); } } public class TypeHintContractResolver : DefaultContractResolver { public override JsonContract ResolveContract(Type type) { JsonContract contract = base.ResolveContract(type); if ((contract is JsonObjectContract) && ((type == typeof(A)) || (type == typeof(B))) ) // In the real implementation, this is checking against a registry of types { contract.Converter = new TypeHintJsonConverter(type); } return contract; } } public class TypeHintJsonConverter : JsonConverter { private readonly Type _declaredType; public TypeHintJsonConverter(Type declaredType) { _declaredType = declaredType; } public override bool CanConvert(Type objectType) { return objectType == _declaredType; } // The real implementation of the next 2 methods uses reflection on concrete types to determine the declaredType hint. // TypeFromTypeHint and TypeHintPropertyForType are the inverse of each other. private Type TypeFromTypeHint(JObject jo) { if (new JValue("B").Equals(jo["Target"])) { return typeof(B); } else if (new JValue("A").Equals(jo["Hint"])) { return typeof(A); } else if (new JValue("C").Equals(jo["Is"])) { return typeof(C); } else { throw new ArgumentException("Type not recognized from JSON"); } } private JProperty TypeHintPropertyForType(Type type) { if (type == typeof(A)) { return new JProperty("Hint", "A"); } else if (type == typeof(B)) { return new JProperty("Target", "B"); } else if (type == typeof(C)) { return new JProperty("Is", "C"); } else { return null; } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (! CanConvert(objectType)) { throw new InvalidOperationException("Can't convert declaredType " + objectType + "; expected " + _declaredType); } // Load JObject from stream. Turns out we're also called for null arrays of our objects, // so handle a null by returning one. var jToken = JToken.Load(reader); if (jToken.Type == JTokenType.Null) return null; if (jToken.Type != JTokenType.Object) { throw new InvalidOperationException("Json: expected " + _declaredType + "; got " + jToken.Type); } JObject jObject = (JObject) jToken; // Select the declaredType based on TypeHint Type deserializingType = TypeFromTypeHint(jObject); var target = Activator.CreateInstance(deserializingType); serializer.Populate(jObject.CreateReader(), target); return target; } public override bool CanWrite { get { return true; } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JProperty typeHintProperty = TypeHintPropertyForType(value.GetType()); //BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''. // Same error occurs whether I use the serializer parameter or a separate serializer. JObject jo = JObject.FromObject(value, serializer); if (typeHintProperty != null) { jo.AddFirst(typeHintProperty); } writer.WriteToken(jo.CreateReader()); } } 

正如您所看到的,从转换器内的转换器上调用JObject.FromObject()将导致递归循环。 通常解决方案是(a)在转换器中使用单独的JsonSerializer实例,或者(b)手动序列化属性,正如James在他的回答中指出的那样。 您的情况有点特别,因为这些解决方案都不适合您:如果您使用不了解转换器的单独序列化程序实例,那么您的子对象将不会应用其提示属性。 正如您在评论中提到的那样,完全手动序列化不适用于通用解决方案。

幸运的是,有一个中间立场。 您可以在WriteJson方法中使用一些reflection来获取对象属性,然后从那里委托给JToken.FromObject() 。 转换器将像子属性一样递归调用,但不会为当前对象调用,因此您不会遇到麻烦。 对此解决方案的一个警告:如果您碰巧将任何[JsonProperty]属性应用于此转换器处理的类(在您的示例中为A,B和C),则不会遵守这些属性。

这是WriteJson方法的更新代码:

 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JProperty typeHintProperty = TypeHintPropertyForType(value.GetType()); JObject jo = new JObject(); if (typeHintProperty != null) { jo.Add(typeHintProperty); } foreach (PropertyInfo prop in value.GetType().GetProperties()) { if (prop.CanRead) { object propValue = prop.GetValue(value); if (propValue != null) { jo.Add(prop.Name, JToken.FromObject(propValue, serializer)); } } } jo.WriteTo(writer); } 

小提琴: https : //dotnetfiddle.net/jQrxb8

使用自定义转换器获取我们忽略的属性的示例,将其分解并将其属性添加到其父对象:

 public class ContextBaseSerializer : JsonConverter { public override bool CanConvert(Type objectType) { return typeof(ContextBase).GetTypeInfo().IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var contextBase = value as ContextBase; var valueToken = JToken.FromObject(value, new ForcedObjectSerializer()); if (contextBase.Properties != null) { var propertiesToken = JToken.FromObject(contextBase.Properties); foreach (var property in propertiesToken.Children()) { valueToken[property.Name] = property.Value; } } valueToken.WriteTo(writer); } } 

我们必须覆盖序列化程序,以便我们指定自定义解析器:

 public class ForcedObjectSerializer : JsonSerializer { public ForcedObjectSerializer() : base() { this.ContractResolver = new ForcedObjectResolver(); } } 

在自定义解析器中,我们将从JsonContract中删除转换器,这将强制内部序列化程序使用默认对象序列化程序:

 public class ForcedObjectResolver : DefaultContractResolver { public override JsonContract ResolveContract(Type type) { // We're going to null the converter to force it to serialize this as a plain object. var contract = base.ResolveContract(type); contract.Converter = null; return contract; } } 

那会让你到那里,或者足够近。 :)我在https://github.com/RoushTech/SegmentDotNet/中使用它,其中包含涵盖此用例的测试用例(包括嵌套我们的自定义序列化类),详细讨论内容如下: https : //github.com /JamesNK/Newtonsoft.Json/issues/386

这个怎么样:

 public class TypeHintContractResolver : DefaultContractResolver { protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { IList result = base.CreateProperties(type, memberSerialization); if (type == typeof(A)) { result.Add(CreateTypeHintProperty(type,"Hint", "A")); } else if (type == typeof(B)) { result.Add(CreateTypeHintProperty(type,"Target", "B")); } else if (type == typeof(C)) { result.Add(CreateTypeHintProperty(type,"Is", "C")); } return result; } private JsonProperty CreateTypeHintProperty(Type declaringType, string propertyName, string propertyValue) { return new JsonProperty { PropertyType = typeof (string), DeclaringType = declaringType, PropertyName = propertyName, ValueProvider = new TypeHintValueProvider(propertyValue), Readable = false, Writable = true }; } } 

需要的类型值提供程序可以像这样简单:

 public class TypeHintValueProvider : IValueProvider { private readonly string _value; public TypeHintValueProvider(string value) { _value = value; } public void SetValue(object target, object value) { } public object GetValue(object target) { return _value; } } 

小提琴: https : //dotnetfiddle.net/DRNzz8

串行器正在调用转换器,然后调用串行器调用转换器等。

使用没有转换器的序列化程序的新实例与JObject.FromObject,或手动序列化类型的成员。

我遇到了类似的问题,这就是我在合同解析器中所做的事情

 if (contract is JsonObjectContract && ShouldUseConverter(type)) { if (contract.Converter is TypeHintJsonConverter) { contract.Converter = null; } else { contract.Converter = new TypeHintJsonConverter(type); } } 

这是我发现避免StackOverflowException的唯一方法。 实际上,每隔一个呼叫都不会使用转换器。

Brian的答案很棒,应该有助于OP,但答案有一些其他人可能会遇到的问题,即:1)序列化数组属性时抛出溢出exception,2)任何静态公共属性都将发送到JSON,你可能不想要。

这是另一个解决这些问题的版本:

 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { Type valueType = value.GetType(); if (valueType.IsArray) { var jArray = new JArray(); foreach (var item in (IEnumerable)value) jArray.Add(JToken.FromObject(item, serializer)); jArray.WriteTo(writer); } else { JProperty typeHintProperty = TypeHintPropertyForType(value.GetType()); var jObj = new JObject(); if (typeHintProperty != null) jo.Add(typeHintProperty); foreach (PropertyInfo property in valueType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (property.CanRead) { object propertyValue = property.GetValue(value); if (propertyValue != null) jObj.Add(property.Name, JToken.FromObject(propertyValue, serializer)); } } jObj.WriteTo(writer); } } 

在遇到同样的问题,并找到这个和其他类似的问题后,我发现JsonConverter有一个过度可用的属性CanWrite。

覆盖此属性以返回false为我修复此问题。

 public override bool CanWrite { get { return false; } } 

希望,这将有助于其他人遇到同样的问题。