43

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

我有一个类型层次结构,我需要能够序列化到 Json 和从 Json 序列化,所以我从这个 SO 中获取代码:如何在 JSON.NET 中实现自定义 JsonConverter 以反序列化基类对象列表?,并将转换器应用于我的层次结构的基类;像这样的东西(这里有伪代码来隐藏不相关的内容):

[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,那么它将被反序列化回该类型的实例。BaseTypeMemberDerivedType

作为记录,这些 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'再次 - 如果我删除转换器属性(禁用我的自定义创建),那么它工作正常......

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

那么我应该编写什么代码来WriteObject执行相同的“标准”序列化?

4

7 回答 7

56

嗯,这很有趣……

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

所以看起来序列化器正在做:

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

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

在 DLL 上打开 ILSpy(是的,我知道它是开源的 - 但我想要“调用者”功能!)并将调用堆栈从 向上移动SerializeConvertableJsonSerializerInternalWriter.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 中的一个错误 - 好吧,这是一个非常极端的情况,它有从宇宙边缘掉下来的危险 - 但如果你确实发现自己处于这种情况,那么你就会陷入困境!

于 2012-09-07T09:31:51.933 回答
8

我在使用 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);
        }
    }
于 2014-09-16T11:46:03.010 回答
2

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

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

  • 创建您正在使用 Converter 的现有类的副本,并将其命名为不同的名称。
  • 删除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然后在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);
    }
}
于 2012-09-09T02:35:02.293 回答
1

我刚刚遇到了与父/子集合相同的问题,发现那个帖子解决了我的问题。我只想显示父集合项的列表并且不需要任何子数据,因此我使用以下内容并且效果很好:

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

它还引用了 Json.NET 代码复合体页面:

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

于 2012-11-25T10:04:34.723 回答
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
于 2017-10-26T07:24:22.023 回答
0

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

JSON.NET 自引用错误

于 2014-02-03T06:19:04.567 回答
0

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

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

dynamic table = new ExpandoObject();
..
..
table.rows = table; <<<<<<<< I assigned same dynamic object to itself. 
于 2015-10-29T16:08:17.860 回答