2014-09-30 62 views
12

我一直無法找到JsonConvert.WriteJson的合理實現,它允許我在序列化特定類型時插入JSON屬性。我所有的嘗試都會導致「JsonSerializationException:使用類型XXX檢測到的自引用循環」。Json.NET,如何定製序列化以插入JSON屬性

我想解決的問題多一點背景:我使用JSON作爲配置文件格式,並且我使用JsonConverter來控制我的配置類型的類型解析,序列化和反序列化。而不是使用$type屬性,我想使用更有意義的JSON值來解析正確的類型。

在我的削減的例子,這裏的一些JSON文本:

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

在JSON屬性"Target": "B"被用來確定這個對象應該被序列化爲B類型。考慮到這個簡單的例子,這種設計可能看起來並不那麼引人注目,但它確實使配置文件格式更加有用。

我也希望配置文件是可以四捨五入的。我有反序列化的情況下工作,我無法工作的是序列化的情況。

我的問題的根源是,我找不到使用標準JSON序列化邏輯的JsonConverter.WriteJson的實現,並且不會拋出「自引用循環」異常。下面是我實現的:

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的示例(例如Custom conversion of specific objects in JSON.NET)僅使用JsonWriter方法寫出單個對象和屬性,從而提供了特定類的自定義序列化。

下面是一個xUnit的測試的完整代碼顯示出我的問題(或see it here

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 {} 

/// <summary> 
/// Shows the problem I'm having serializing classes with Json. 
/// </summary> 
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<A>(json, jsonSettings); 

     Assert.IsType<B>(a); 
     Assert.IsType<C>(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()); 
    } 

} 
+1

在你的轉換器的WriteJson方法中, 'JObject的'serializer'參數。FromObject()'調用完全?似乎在[這個小提琴](https://dotnetfiddle.net/lZfCWJ) – 2014-09-30 22:06:05

+0

工作謝謝布賴恩 - 謝謝你看這個,你是對的,修復了異常。它不,但是解決我的問題,因爲我需要能夠在嵌套對象中執行此操作。我已經更新了這個例子。或者,請參閱https://dotnetfiddle.net/b3yrEU(Fiddle is COOL !!) – crimbo 2014-10-01 06:49:58

+1

我會很有興趣瞭解最終結果。我有同樣的問題。 – 2015-07-14 05:33:19

回答

1

串行器被調用到您的轉換器中,然後調用到被調用到您的轉換器串行器等

要麼使用沒有使用JObject.FromObject的轉換器的序列化程序的新實例,要麼手動序列化類型的成員。

+1

謝謝。序列化我的類型的成員手動是不可行的,因爲這是一個普遍的問題,我需要它與任何配置的類型一起工作。我正在尋找的是一種攔截正常序列化邏輯來插入屬性的方法。理想情況下,它也會使用原始序列化程序中的任何其他自定義序列化設置,但現在我可以沒有。 我會嘗試使用第二個串行器和JObject。 – crimbo 2014-10-07 19:39:44

9

從轉換器中調用JObject.FromObject()轉換到同一個被轉換的對象將會產生一個遞歸循環,就像你所看到的那樣。正常情況下,解決方案是(a)在轉換器內部使用單獨的JsonSerializer實例,或者(b)手動序列化屬性,正如James在答案中指出的那樣。您的情況有點特別,因爲這些解決方案都不適合您:如果您使用不知道轉換器的單獨序列化程序實例,那麼您的子對象將無法應用它們的提示屬性。和序列化完全手動不適用於一般化的解決方案,正如你在評論中提到的那樣。

幸運的是,有一箇中間地帶。您可以在WriteJson方法中使用一些反射來獲取對象屬性,然後從那裏委託給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

1

我也曾有過類似的問題,這裏就是我的合同解析器做

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

這是我發現避免StackOverflowException的唯一方法。實際上,其他每個通話都不會使用轉換器。

-2

在發現相同問題並找到此問題和其他類似問題後,我發現JsonConverter具有可重寫屬性CanWrite。

覆蓋此屬性返回false爲我解決了這個問題。

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

希望這可以幫助他人解決同樣的問題。

+0

通過從http://stackoverflow.com/a/9444519/1037948稍微修改的解決方案做到這一點,最近我開始從'WriteJson'獲得'NotImplementedException',並且不知道爲什麼。我懷疑這是因爲我也覆蓋了'CanConvert'。只是想指出。 – drzaus 2015-07-01 15:56:45

+3

如果'CanWrite'返回false'WriteJson'完全沒有被調用。 – 2015-07-14 05:36:35

3

如何:

public class TypeHintContractResolver : DefaultContractResolver 
{ 

    protected override IList<JsonProperty> CreateProperties(Type type, 
     MemberSerialization memberSerialization) 
    { 
    IList<JsonProperty> 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 
    }; 
    } 
} 

爲所需要的類型值提供者可以如此簡單:

​​

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

+0

這看起來像什麼後,但我想用它來序列化,但我不能讓它工作(屬性得到添加,但不出現在JSON瀏覽器)。它是否適用於序列化? – 2017-10-03 17:49:03

1

布賴恩的答案是偉大的,應該幫助操作系統,但答案有一些其他人可能會遇到的問題,即:1)序列化數組屬性時引發溢出異常,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); 
    } 
} 
4

使用自定義轉換器把我們忽略的屬性,打破它,並添加它的屬性它的實例的父對象:

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<JProperty>()) 
      { 
       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

+1

這很容易成爲這裏最被低估的答案。對於它的價值,這不是一個完美的解決方案,因爲我真的想使用原始序列化程序的所有設置。 [看看這個答案](http://stackoverflow.com/a/38230327/2283050)。你可以考慮改進這個答案以反映收益。仍然是令人驚歎的工作 – 2016-07-06 17:42:58

相關問題