代码之家  ›  专栏  ›  技术社区  ›  Andras Zoltan

来自自定义JsonConverter(Web API)的Json.Net JsonSerializer中的自引用循环

  •  43
  • Andras Zoltan  · 技术社区  · 13 年前

    该项目是一个Asp.Net Web API Web服务。

    我有一个类型层次结构,我需要能够序列化到Json和从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讨论,你会发现我使用的基本代码完全是围绕反序列化的;其使用示例显示了串行器的手动创建。这个 JsonConverter 由此带来的实现 JsonCreationConverter<T> 只是抛出一个 NotImplementedException

    现在,由于Web API对请求使用单个格式化程序的方式,我需要在 WriteObject 方法

    在这一点上,我必须强调,在开始我项目的这一部分之前,我 每件事 正确序列化 没有错误

    所以我这样做了:

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

    但我得到了 JsonSerializationException 以下为: Self referencing loop detected with type 'DerivedType' ,当其中一个对象被序列化时。同样,如果我删除转换器属性(禁用我的自定义创建),那么它可以正常工作。。。

    我有一种感觉,这意味着我的序列化代码实际上在同一个对象上再次触发转换器,而转换器又再次调用序列化程序——令人恶心。 已确认-查看我的答案

    那么什么代码 应该 我正在写 写入对象 将执行相同的“标准”序列化?

    7 回复  |  直到 13 年前
        1
  •  56
  •   Andras Zoltan    13 年前

    这很有趣。。。

    当我更仔细地查看异常的堆栈跟踪时,我注意到 JsonSerializerInternalWriter.SerializeConvertable 在那里有两次,实际上是堆栈顶部的方法一——调用 JsonSerializerInternalWriter.CheckForCircularReference -这反过来又抛出了异常。然而,它也是调用我自己的转换器的源 Write 方法

    因此,序列化程序似乎在做:

    • 1) 如果对象有转换器
      • 1a)如果循环参考,则抛出
      • 1b)调用转换器的Write方法
    • 2) 其他
      • 2a)使用内部串行器

    因此,在这种情况下,Json.Net正在调用我的转换器,而转换器又在调用Json.Net序列化程序,然后它会爆炸,因为它看到它已经在序列化传递给它的对象!

    在DLL上打开ILSpy(是的,我知道它是开源的,但我想要“callers”功能!)并从 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中的代码复制的基本转换器中,或者添加到派生SO中:

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

    现在一切都很好。

    然而,这样做的结果是,如果您打算对对象进行一些自定义JSON序列化,并且正在为其注入转换器 您打算在某些或所有情况下回退到标准序列化机制;那么你不能这样做,因为你会欺骗框架,让它认为你试图存储一个循环引用。

    我确实试过操纵 ReferenceLoopHandling 成员,但如果我告诉它 Ignore 然后什么都没有序列化,如果我告诉它保存它们,毫不奇怪,我会出现堆栈溢出。

    这可能是Json.Net中的一个bug——好吧,这是一个非常边缘的情况,它有从宇宙边缘掉下来的危险——但如果你真的发现自己处于这种情况,那么你就有点被卡住了!

        2
  •  8
  •   Ian Gibson    11 年前

    我在使用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);
            }
        }
    
        3
  •  2
  •   Dave R.    13 年前

    我自己刚刚遇到这个,我沮丧地把头发拔了出来!

    为了解决这个问题,以下对我有效,但因为我错过了 CanWrite 解决方案,这是一个更复杂的解决方案。

    • 创建您正在使用转换器的现有类的副本,并对其进行不同的调用。
    • 移除 JsonConverter 属性。
    • 在新类上创建一个构造函数,该构造函数采用与原始类相同类型的参数。使用构造函数复制以后序列化所需的任何值。
    • 在转换器中 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或更高版本。方法是创建一个新的 dynamic ExpandoObject 然后在中初始化其属性 写入Json 方法直接创建副本,例如:

    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);
        }
    }
    
        4
  •  1
  •   Muhammad Omar ElShourbagy    13 年前

    我刚刚在Parent/Child收藏中遇到了同样的问题,并找到了解决我问题的帖子。我只想显示父集合项的列表,不需要任何子数据,因此我使用了以下内容,效果很好:

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

    它还引用了Json.NET密码丛页面,位于:

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

        5
  •  1
  •   2stroke    8 年前

    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
    
        6
  •  0
  •   ProVega    12 年前

    这可能会对某些人有所帮助,但在我的情况下,我试图重写Equals方法,使我的对象被视为值类型。在我的研究中,我发现JSON.NET不喜欢这样:

    JSON.NET Self Referencing Error

        7
  •  0
  •   dvdmn    10 年前

    我的是一个简单的错误,与这个问题的解决无关。

    这个话题是谷歌的第一页,所以我在这里发帖,以防其他人和我有同样的问题。

    dynamic table = new ExpandoObject();
    ..
    ..
    table.rows = table; <<<<<<<< I assigned same dynamic object to itself. 
    
    推荐文章