2012-09-07 55 views
34

該項目是一個Asp.Net Web API Web服務。自定義JsonConverter(Web API)中的Json.Net JsonSerializer中的自引用循環

我有一個類型層次,我需要能夠序列化和JSON,所以我採取的代碼從這個SO:How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?,並應用了轉換器到我的層次結構的基類;像這樣(有僞代碼這裏隱藏不相干):

[JsonConverter(typeof(TheConverter))] 
public class BaseType{ 
    ///note the base of this type here is from the linked SO above 
    private class TheConverter : JsonCreationConverter<BaseType>{ 
    protected override BaseType Create(Type objectType, JObject jObject){ 
     Type actualType = GetTypeFromjObject(jObject); /*method elided*/ 
     return (BaseType)Activator.CreateInstance(actualType); 
    } 
    } 
} 

public class RootType { 
    public BaseType BaseTypeMember { get; set; } 
} 

public class DerivedType : BaseType {  
} 

所以,如果我反序列化RootType實例,其BaseTypeMember是等於DerivedType一個實例,然後將它反序列化回那個類型的實例。爲了記錄,這些JSON對象包含一個'$type'字段,其​​中包含虛擬類型名稱(不是完整的.Net類型名稱),所以我可以同時支持JSON中的類型,同時準確控制哪些類型可以序列化和反序列化。

現在,這對於反序列化請求中的值非常有效;但是我有序列化問題。如果你看一下鏈接的SO,以及從頂部答案鏈接的Json.Net討論,你會發現我使用的基本代碼完全圍繞反序列化進行;以及其使用示例顯示手動創建串行器。由JsonCreationConverter<T>帶來的JsonConverter執行簡單地拋出一個NotImplementedException

現在,由於Web API使用單個格式化程序進行請求的方式,我需要在WriteObject方法中實現「標準」序列化。

我必須在這一點上強調,在開始我的這部分項目之前,我有一切序列化正確沒有錯誤

所以我這樣做:

public override void WriteJson(JsonWriter writer, 
    object value, 
    JsonSerializer serializer) 
{ 
    serializer.Serialize(writer, value); 
} 

但我得到一個JsonSerializationExceptionSelf referencing loop detected with type 'DerivedType',當一個對象序列化。再次 - 如果我刪除轉換器屬性(禁用我的自定義創建),那麼它工作正常...

我有一種感覺,這意味着我的序列化代碼實際上是在同一個對象上再次觸發轉換器,反過來再次調用序列化器 - 令人厭惡。 確認 - 看到我的回答

那麼什麼碼應該我可以在WriteObject書面會做同樣的「標準」序列化的作品?

+1

尼斯Q&A,但值得指出的是串行器是做正確的事情:它檢查尋找自定義序列化器的對象並找到一個。你應該擔心,如果它做了別的事情,否則它可能不尊重外國類的定製轉換器。 –

+0

@RupertRawnsley是的我同意 –

回答

49

嗯,這很有趣......

當我在爲異常堆棧跟蹤更加仔細地看了看,我注意到法JsonSerializerInternalWriter.SerializeConvertable在那裏兩次,其實這也是該方法一次性的頂部堆棧 - 調用JsonSerializerInternalWriter.CheckForCircularReference - 反過來拋出異常。然而,它也是我自己的轉換器Write方法的來源。

所以它似乎是序列化是這樣做的:

  • 1)如果對象有一個轉換器
    • 1A)柔道如果循環引用
    • 1b)的調用轉換器的Write方法
  • 2)其他
    • 2a)使用內部串行器

因此,在這種情況下,Json.Net呼喚我的轉換器,其又調用Json.Net串行器,然後炸燬了,因爲它認爲它已經序列化傳遞給它的對象!

開幕ILSpy的DLL(是的,我知道它是開源的 - 但我想「來電」的功能),並從SerializeConvertable調用棧向上移動到JsonSerializerInternalWriter.SerializeValue,該檢測器是否應使用可代碼找到正確的附近開始:

if (((jsonConverter = ((member != null) ? member.Converter : null)) != null 
    || (jsonConverter = ((containerProperty != null) ? containerProperty.ItemConverter 
                : null)) != null 
    || (jsonConverter = ((containerContract != null) ? containerContract.ItemConverter 
                : null)) != null 
    || (jsonConverter = valueContract.Converter) != null 
    || (jsonConverter = 
     this.Serializer.GetMatchingConverter(valueContract.UnderlyingType)) != null 
    || (jsonConverter = valueContract.InternalConverter) != null) 
    && jsonConverter.CanWrite) 
{ 
    this.SerializeConvertable(writer, jsonConverter, value, valueContract, 
           containerContract, containerProperty); 
    return; 
} 

值得慶幸的是在if聲明最後條件提供瞭解決我的問題:首先,我要做的就是下面的添加到任何基地轉換器從代碼複製在問題中或在派生的一箇中連接的SO:

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

現在一切正常。

這樣的結果,但是,如果你想有一個物體上一些自定義的JSON序列化和你是一個轉換器將其注入你打算回退到標準序列化機制下的一些或所有情況;那麼你不能這樣做,因爲你會欺騙框架,認爲你試圖存儲循環引用。

我確實嘗試過操縱ReferenceLoopHandling成員,但是如果我告訴它Ignore那麼它們就沒有任何序列化,如果我告訴它保存它們,毫不奇怪,我得到了堆棧溢出。

這可能是Json.Net中的一個錯誤 - 好吧它有很多邊緣情況下有可能從宇宙邊緣掉下來 - 但是如果你確實發現自己處於這種情況下,有點卡住了!

+2

我不得不說這是不具優勢的情況下,有很多與此相關的問題與定製JsonConverters線程。我欣賞這個帖子。謝謝! – GFXGunblade

+0

這完全糟透了。您必須手動嘗試嵌入了「$型」自己,其框架隱藏您在私有變量和類所需要的一切,所以你甚至不能使用串行withing一個JsonConverter的WriteJson方法。可怕。 – Triynko

+0

我寫了自己的框架,它做對了。它允許您讀取或寫入動態對象,並處理在調用之外嵌入類型名稱和其他特殊參數。 – Triynko

2

我剛剛遇到這個,我沮喪地拉着我的頭髮!

爲了解決這個問題,以下工作對我來說很重要,但是由於我錯過了CanWrite解決方案,這是一個更復雜的解決方法。

  • 創建您正在使用Converter的現有類的副本,並將其稱爲不同的東西。
  • 刪除副本上的JsonConverter屬性。
  • 在新類中創建一個構造函數,該類需要與原始類具有相同類型的參數。使用構造函數複製以後序列化所需的任何值。
  • 在您的Converter的WriteJson方法中,將該值轉換爲您的虛擬類型,然後將該類型序列化。

舉例來說,這類似於我的原始類:

[JsonConverter(typeof(MyResponseConverter))] 
public class MyResponse 
{ 
    public ResponseBlog blog { get; set; } 
    public Post[] posts { get; set; } 
} 

副本看起來是這樣的:

public class FakeMyResponse 
{ 
    public ResponseBlog blog { get; set; } 
    public Post[] posts { get; set; } 

    public FakeMyResponse(MyResponse response) 
    { 
     blog = response.blog; 
     posts = response.posts; 
    } 
} 

的WriteJson是:

public override void WriteJson(JsonWriter writer, object value, 
    JsonSerializer serializer) 
{ 
    if (CanConvert(value.GetType())) 
    { 
     FakeMyResponse response = new FakeMyResponse((MyResponse)value); 
     serializer.Serialize(writer, response); 
    } 
} 

編輯:

的OP指出,使用Expando可能是另一種可能的解決方案。這很好,節省了創建新類的麻煩,儘管DLR支持需要Framework 4.0或更高版本。該方法是創建一個新的dynamicExpandoObject,然後在WriteJson方法初始化它的屬性中直接創建副本,如:

public override void WriteJson(JsonWriter writer, object value, 
    JsonSerializer serializer) 
{ 
    if (CanConvert(value.GetType())) 
    { 
     var response = (MyResponse)value; 
     dynamic fake = new System.Dynamic.ExpandoObject(); 
     fake.blog = response.blog; 
     fake.posts = response.posts; 
     serializer.Serialize(writer, fake); 
    } 
} 
+1

這是一個新的解決方案的時候,你真的需要同時使用轉換器和標準系列化嚴重的問題 - 因爲我們的JSON序列化對象*往往*要輕,具有複製不是那些太糟糕的情況。我想,還有,Expando也可能工作。 –

+0

Expando解決方案非常優雅。我可以看到不少其他地方在我的項目中有用。謝謝:) –

+0

哦,是的,除非你完全失去了類型信息。這將不是一個「$類型」字段寫入對象,所以忘了反序列化它作爲一個類型的對象,尤其是如果目標成員是一個接口,需要嵌入了堅實的類型。另外,框架隱藏了一切,你需要寫的「$型」(更不用說值對象的$ id)的私有變量,使得它幾乎不可能自己嵌入這樣的值。 JSON.NET框架存在嚴重的設計缺陷;幾乎無法使用。 – Triynko

2

我剛剛與父/子集相同plroblem,發現帖子裏面有解決了我的情況。我只是想表明父集合項目的名單,並沒有需要任何的子數據的,因此我用下面的,它工作得很好:

JsonConvert.SerializeObject(ResultGroups, Formatting.None, 
         new JsonSerializerSettings() 
         { 
          ReferenceLoopHandling = ReferenceLoopHandling.Ignore 
         }); 

也referes到Json.NET codplex在頁面:

http://json.codeplex.com/discussions/272371

+0

讚賞這一點,但這在這個特定問題中所闡述的情況下不起作用。 –

+0

對不起安德拉斯,我認爲JsonSerializerSettings會有所幫助。 –

+0

簡單易用的解決方案,效果很好 - 謝謝 –

0

這可能幫助別人,但對我來說,我試圖重寫Equals方法讓我的對象是值類型對待。在我的研究,我發現,JSON.NET不喜歡這樣的:

JSON.NET Self Referencing Error

6

我遇到使用Newtonsoft.Json版本4.5.7.15008這個問題。我嘗試了這裏提供的所有解決方案以及其他一些解決方案。我使用下面的代碼解決了這個問題。基本上你可以使用另一個JsonSerializer來執行序列化。創建的JsonSerializer沒有任何已註冊的轉換器,因此可以避免重新入口/異常。如果使用其他設置或ContractResolver,則需要手動將其設置爲所創建的序列化:可以將一些構造函數參數添加到CustomConverter類以適應此情況。

public class CustomConverter : JsonConverter 
    { 
     /// <summary> 
     /// Use a privately create serializer so we don't re-enter into CanConvert and cause a Newtonsoft exception 
     /// </summary> 
     private readonly JsonSerializer noRegisteredConvertersSerializer = new JsonSerializer(); 

     public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 
     { 
      bool meetsCondition = false; /* add condition here */ 
      if (!meetsCondition) 
       writer.WriteNull(); 
      else 
       noRegisteredConvertersSerializer.Serialize(writer, value); 
     } 

     public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 
     { 
      throw new NotImplementedException(); 
     } 

     public override bool CanConvert(Type objectType) 
     { 
      // example: register accepted conversion types here 
      return typeof(IDictionary<string, object>).IsAssignableFrom(objectType); 
     } 
    } 
+1

這是一個簡單的解決方案,以允許回退的再入口問題 - 像它:) –

0

礦是一個簡單的錯誤,並且與本主題的解決方案無關。

此主題是谷歌的第一頁,所以我張貼在這裏,以防其他人會有同樣的問題,因爲我做了。

dynamic table = new ExpandoObject(); 
.. 
.. 
table.rows = table; <<<<<<<< I assigned same dynamic object to itself. 
1

IMO,這是圖書館的嚴重限制。解決方案非常簡單,但我承認它很快就沒有到我這裏來。解決的辦法是設置:

.ReferenceLoopHandling = ReferenceLoopHandling.Serialize 

其中,如記錄所有的地方,將消除自參考錯誤和一個堆棧溢出更換。在我的情況下,我需要寫功能,所以將CanWrite設置爲false不是一個選項。最後我只是設置一個標誌看守CanConvert通話時,我知道,串行調用會導致(無盡)遞歸:

Public Class ReferencingObjectConverter : Inherits JsonConverter 

     Private _objects As New HashSet(Of String) 
     Private _ignoreNext As Boolean = False 

     Public Overrides Function CanConvert(objectType As Type) As Boolean 
      If Not _ignoreNext Then 
       Return GetType(IElement).IsAssignableFrom(objectType) AndAlso Not GetType(IdProperty).IsAssignableFrom(objectType) 
      Else 
       _ignoreNext = False 
       Return False 
      End If 
     End Function 

     Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer) 

      Try 
       If _objects.Contains(CType(value, IElement).Id.Value) Then 'insert a reference to existing serialized object 
        serializer.Serialize(writer, New Reference With {.Reference = CType(value, IElement).Id.Value}) 
       Else 'add to my list of processed objects 
        _objects.Add(CType(value, IElement).Id.Value) 
        'the serialize will trigger a call to CanConvert (which is how we got here it the first place) 
        'and will bring us right back here with the same 'value' parameter (and SO eventually), so flag 
        'the CanConvert function to skip the next call. 
        _ignoreNext = True 
        serializer.Serialize(writer, value) 
       End If 
      Catch ex As Exception 
       Trace.WriteLine(ex.ToString) 
      End Try 

     End Sub 

     Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object 
      Throw New NotImplementedException() 
     End Function 

     Private Class Reference 
      Public Property Reference As String 
     End Class 

    End Class 
相關問題