自定义JsonConverter(Web API)中的Json.Net JsonSerializer中的自引用循环
该项目是一个Asp.Net Web API Web服务。
我有一个types层次结构,我需要能够序列化到和从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
其BaseTypeMember
等于DerivedType
的实例的DerivedType
实例,则它将被反序列化回该types的实例。
为了logging,这些JSON对象包含一个'$type'
字段,其中包含虚拟types名称(不是完整的.Nettypes名称),所以我可以同时在JSON中支持types,同时准确控制哪些types可以序列化和反序列化。
现在这对于请求中的值的反序列化非常有效。 但我有序列化的问题。 如果你看一下链接的SO,以及从顶部答案中链接到的Json.Net讨论,你会发现我使用的基本代码完全是围绕反序列化进行的; 用其示例来显示手动创build串行器。 由JsonCreationConverter<T>
带到表中的JsonConverter
实现只是抛出一个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'
。 再次 – 如果我删除转换器属性(禁用我的自定义创build),那么它工作正常…
我有一种感觉,这意味着我的序列化代码实际上是在同一个对象上再次触发转换器,然后再次调用序列化器 – 令人厌恶。 确认 – 看我的答案
那么,我应该在WriteObject
中写什么代码来执行相同的“标准”序列化?
那么这很有趣…
当我更仔细地查看exception的堆栈跟踪时,我注意到JsonSerializerInternalWriter.SerializeConvertable
方法在那里两次,实际上是那个方法离堆栈的顶端 – 调用JsonSerializerInternalWriter.CheckForCircularReference
– 这反过来扔了例外。 但是,这也是我自己的转换器的Write
方法的来源。
所以看起来这个串行器在做:
- 1)如果对象有一个转换器
- 1a)如果循环参考,则抛出
- 1b)调用转换器的写入方法
- 2)否则
- 2a)使用内部序列化器
所以,在这种情况下,Json.Net调用我的转换器,而转换器又调用Json.Net序列化器,然后就会爆炸,因为它看到它已经序列化了传递给它的对象!
在DLL上打开ILSpy(是的,我知道它是开源的 – 但我想'调用者'的function!)和调用堆栈从SerializeConvertable
上移动到JsonSerializerInternalWriter.SerializeValue
,检测是否应该使用转换器的代码可以find在开始附近:
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
语句中的最后一个条件为我的问题提供了解决scheme:我所要做的只是将以下内容添加到从问题中链接的SO中的代码复制的基本转换器中,或者添加到派生的:
public override bool CanWrite { get { return false; } }
现在一切正常。
然而,这样做的结果是,如果你打算在一个对象上有一些自定义的JSON序列化,并且你正在用一个转换器注入它, 并且你打算在某些或所有情况下回退到标准的序列化机制。 那么你不能,因为你会愚弄框架,认为你正在试图存储循环引用。
我曾尝试操作ReferenceLoopHandling
成员,但如果我告诉它Ignore
它们,那么没有任何序列化,如果我告诉它保存它们,毫不奇怪,我得到了堆栈溢出。
这可能是Json.Net中的一个bug – 好吧,它有很多的边缘情况,以至于有可能从宇宙的边缘掉下来 – 但是如果你发现自己处在这种情况下,那么你会陷入困境!
我遇到了使用Newtonsoft.Json 4.5.7.15008版本的问题。 我尝试了与其他一些解决scheme一起提供的所有解决scheme。 我使用下面的代码解决了这个问题。 基本上你可以使用另一个JsonSerializer来执行序列化。 创build的JsonSerializer没有任何已注册的转换器,因此可以避免重新入口/exception。 如果使用其他设置或ContractResolver,则需要手动将其设置为所创build的序列化:可以将一些构造函数参数添加到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); } }
我只是自己碰到了这个,我沮丧地把头发拉出来!
要解决这个问题,下面的工作对我来说,但是因为我错过了CanWrite
解决scheme,这是一个更复杂的解决方法。
- 创build一个你使用你的Converter的现有类的副本,并调用它不同的东西。
- 删除副本上的
JsonConverter
属性。 - 在新类上创build一个构造函数,该类需要与原始类具有相同types的参数。 使用构造函数复制稍后序列化所需的任何值。
- 在您的Converter的
WriteJson
方法中,将该值转换为您的虚拟types,然后序列化该types。
例如,这与我原来的类相似:
[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可能是另一个可能的解决scheme。 这很好,节省了创build新类的麻烦,尽pipeDLR支持需要Framework 4.0或更高版本。 该方法是创build一个新的dynamic
ExpandoObject
,然后直接在WriteJson
方法中初始化其属性以创build副本,例如:
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); } }
我刚刚和父母/孩子collections一样,发现这个post解决了我的情况。 我只想显示父集合项目的列表,并不需要任何的子数据,因此我使用以下,它工作正常:
JsonConvert.SerializeObject(ResultGroups, Formatting.None, new JsonSerializerSettings() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore });
它也反驳了Json.NET codplex页面:
海事组织,这是图书馆的一个严重的局限性。 解决办法很简单,虽然我承认这不是很快就到了。 解决scheme是设置:
.ReferenceLoopHandling = ReferenceLoopHandling.Serialize
如同遍布全球的文档所述,将消除自引用错误并将其replace为堆栈溢出。 在我的情况下,我需要写function,所以设置CanWrite为false不是一个选项。 最后,我只是设置了一个标志,以防止CanConvert调用,当我知道对串行器的调用正在进行原因(无尽)recursion:
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
这可能有助于某人,但在我的情况下,我试图重写Equals方法来将我的对象视为值types。 在我的研究中,我发现JSON.NET不喜欢这样的:
JSON.NET自引用错误
我是一个简单的错误,与这个话题的解决无关。
这个主题是谷歌的第一页,所以我在这里发帖,以防其他人会有同样的问题。
dynamic table = new ExpandoObject(); .. .. table.rows = table; <<<<<<<< I assigned same dynamic object to itself.