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

时间:2012-09-07 08:28:14

标签: c# asp.net-web-api json.net

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

我有一个类型层次结构,我需要能够与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);
}

但是,当其中一个对象被序列化时,我得到JsonSerializationExceptionSelf referencing loop detected with type 'DerivedType'。再次 - 如果我删除转换器属性(禁用我的自定义创建),那么它工作正常......

我有一种感觉,这意味着我的序列化代码实际上是在同一个对象上再次触发转换器,而这反过来再次调用序列化器 - 令人作呕。 确认 - 请参阅我的回答

那么我应该在WriteObject编写哪些代码 哪个代码可以执行相同的'标准'序列化?

7 个答案:

答案 0 :(得分:52)

这很有趣......

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

所以看起来串行器正在做:

  • 1)如果对象有转换器
    • 1a)抛出循环参考
    • 1b)调用转换器的Write方法
  • 2)其他
    • 2a)使用内部序列化程序

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

在DLL上打开ILSpy(是的,我知道它是开源的 - 但我想要'调用者'的功能!)并将调用堆栈从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中的一个错误 - 好吧,这是一个边缘情况,它有可能从宇宙边缘掉下来 - 但如果你确实发现自己处于这种情况,那么你就是有点卡住了!

答案 1 :(得分: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);
        }
    }

答案 2 :(得分: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或更高版本。方法是创建一个新的dynamic ExpandoObject,然后直接在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);
    }
}

答案 3 :(得分:2)

我和父/子收藏品有同样的问题,发现那个帖子解决了我的问题。我只想显示父集合项列表,并且不需要任何子数据,因此我使用以下内容并且工作正常:

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

它还引用了Json.NET codplex页面:

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

答案 4 :(得分: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

答案 5 :(得分:0)

这可能对某人有所帮助,但在我的情况下,我试图覆盖Equals方法以将我的对象视为值类型。在我的研究中,我发现JSON.NET不是这样的:

JSON.NET Self Referencing Error

答案 6 :(得分:0)

我是一个简单的错误,与此主题的解决方案无关。

此主题是google中的第一页,因此我发布此处以防其他人遇到与我相同的问题。

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