Self referencing loop in Json.Net JsonSerializer from custom JsonConverter (Web API)

前端 未结 7 1242
温柔的废话
温柔的废话 2020-12-16 09:28

The project is an Asp.Net Web API web service.

I have a type hierarchy that I need to be able to serialize to and from Json, so I have taken the code from this SO: H

相关标签:
7条回答
  • 2020-12-16 09:39

    Mine was a simple mistake, and had nothing to do with the solution of this topic.

    This topic was the 1st page in google, so I am posting here in case others would have same issue as I did.

    dynamic table = new ExpandoObject();
    ..
    ..
    table.rows = table; <<<<<<<< I assigned same dynamic object to itself. 
    
    0 讨论(0)
  • 2020-12-16 09:45

    I encountered this problem using version 4.5.7.15008 of Newtonsoft.Json. I tried all of the solutions offered here along with some others. I resolved the issue using the code below. Basically you can just use another JsonSerializer to perform the serialization. The JsonSerializer that is created doesn't have any registered converters so the re-entrance / exception will be avoided. If other settings or ContractResolver are used then they will need to manually set them on the serialized that is created: some constructor arguments could be added to the CustomConverter class to accommodate this.

        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);
            }
        }
    
    0 讨论(0)
  • 2020-12-16 09:52

    I just had the same plroblem with Parent/Child collections and found that post which has solved my case. I Only wanted to show the List of parent collection items and didn't need any of the child data, therefore i use the following and it worked fine:

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

    it also referes to the Json.NET codplex page at:

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

    0 讨论(0)
  • 2020-12-16 09:55

    Well this was fun...

    When I looked more closely at the stack trace for the exception, I noticed that the method JsonSerializerInternalWriter.SerializeConvertable was in there twice, indeed it was that method one off the top of the stack - invoking JsonSerializerInternalWriter.CheckForCircularReference - which in turn was throwing the exception. It was also, however, the source of the call to my own converter's Write method.

    So it would seem that the serializer was doing:

    • 1) If object has a converter
      • 1a) Throw if circular reference
      • 1b) Invoke converter's Write method
    • 2) Else
      • 2a) Use internal serializers

    So, in this case, the Json.Net is calling my converter which in turn is calling the Json.Net serializer which then blows up because it sees it's already serializing the object that was passed to it!

    Opening ILSpy on the DLL (yes I know it's open source - but I want the 'callers' functionality!) and moving up the call stack from SerializeConvertable to JsonSerializerInternalWriter.SerializeValue, the code that detects whether a converter should be used can be found right near the start:

    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;
    }
    

    Thankfully that very last condition in the if statement provides the solution to my issue: all I had to do was to add the following to either the base converter copied from the code in the linked SO in the question, or in the derived one:

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

    And now it all works fine.

    The upshot of this, however, is that if you intend to have some custom JSON serialization on an object and you are injecting it with a converter and you intend to fallback to the standard serialization mechanism under some or all situations; then you can't because you will fool the framework into thinking you're trying to store a circular reference.

    I did try manipulating the ReferenceLoopHandling member, but if I told it to Ignore them then nothing was serialized and if I told it to save them, unsurprisingly, I got a stack overflow.

    It's possible that this is a bug in Json.Net - alright it's so much of an edge-case that it's in danger of falling off the edge of the universe - but if you do find yourself in this situation then you're kind of stuck!

    0 讨论(0)
  • 2020-12-16 09:55

    IMO, this is a serious limitation of the library. The solution is quite simple, although I will admit it didn't come to me that quickly. The solution is to set:

    .ReferenceLoopHandling = ReferenceLoopHandling.Serialize
    

    which, as documented all over the place, will eliminate the self referencing error and replace it with a stack overflow. In my case, I required write functionality, so setting CanWrite to false was not an option. In the end I just set a flag to guard the CanConvert call when I know that a call to the serializer is going cause (endless) 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
    
    0 讨论(0)
  • 2020-12-16 09:56

    I just came across this myself and I was pulling my hair out in frustration!

    To solve the issue, the following worked for me, but because I missed the CanWrite solution, it's a more complex workaround.

    • Create a copy of the existing class on which you're using your Converter and call it something different.
    • Remove the JsonConverter attribute on the copy.
    • Create a constructor on the new class which takes a parameter of the same type as the original class. Use the constructor to copy over any values which are required for later serialisation.
    • In your Converter's WriteJson method, convert the value into your dummy type, then serialise that type instead.

    For instance, this is similar to my original class:

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

    The copy looks like this:

    public class FakeMyResponse
    {
        public ResponseBlog blog { get; set; }
        public Post[] posts { get; set; }
    
        public FakeMyResponse(MyResponse response)
        {
            blog = response.blog;
            posts = response.posts;
        }
    }
    

    The WriteJson is:

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

    Edit:

    The OP pointed out that using an Expando could be another possible solution. This works well, saving the bother of creating the new class, although DLR support requires Framework 4.0 or later. The approach is to create a new dynamic ExpandoObject and then initialise its properties in the WriteJson method directly to create the copy, e.g.:

    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);
        }
    }
    
    0 讨论(0)
提交回复
热议问题