Самостоятельный цикл ссылок в Json.Net JsonSerializer из пользовательского JsonConverter (Web API)

Проект является веб-сервисом веб-API Asp.Net.

У меня есть иерархия типов, в которой мне нужно иметь возможность сериализации в Json и из нее, поэтому я взял код из этого SO: Как реализовать пользовательский JsonConverter в JSON.NET десериализовать Список объектов базового класса? и применить конвертер к базовому классу иерархии; что-то вроде этого (там псевдокод здесь, чтобы скрыть несоответствия):

[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>, просто вызывает a NotImplementedException.

Теперь, из-за того, что веб-API использует один форматтер для запроса, мне нужно реализовать стандартную сериализацию в методе WriteObject.

В этот момент я должен подчеркнуть, что перед тем, как приступить к этой части моего проекта, у меня было все сериализация без ошибок.

Итак, я сделал это:

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

Но я получаю JsonSerializationException: Self referencing loop detected with type 'DerivedType', когда один из объектов сериализуется. Опять же - если я удалю атрибут конвертера (отключив мое собственное создание), тогда он отлично работает...

У меня такое ощущение, что это означает, что мой код сериализации фактически запускает конвертер снова на том же объекте, который, в свою очередь, снова вызывает сериализатор - ad nauseam. Подтверждено - см. мой ответ

Итак, какой код я должен писать в WriteObject, который будет выполнять ту же "стандартную" сериализацию, которая работает?

Ответ 1

Ну это было весело...

Когда я посмотрел более подробно на трассировку стека для исключения, я заметил, что метод JsonSerializerInternalWriter.SerializeConvertable был там дважды, действительно, это был тот метод, который находится сверху стека - вызов JsonSerializerInternalWriter.CheckForCircularReference - который, в свою очередь, бросает исключение. Это также было источником вызова моего собственного метода конвертера Write.

Таким образом, казалось бы, что сериализатор делает:

  • 1) Если объект имеет конвертер
    • 1a) Бросьте, если круговая ссылка
    • 1b) Вызвать метод записи конвертера
  • 2) Else
    • 2a) Использование внутренних сериализаторов

Итак, в этом случае Json.Net вызывает мой конвертер, который, в свою очередь, вызывает сериализатор Json.Net, который затем взрывается, потому что он видит, что он уже сериализует переданный ему объект!

Открытие ILSpy в DLL (да, я знаю его с открытым исходным кодом, но я хочу, чтобы функции "вызывающих"!) и перемещение стека вызовов с 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 - хорошо, что это очень крошечный случай, что он может упасть с края Вселенной, но если вы окажетесь в этой ситуации, тогда вы вид застрял!

Ответ 2

Я столкнулся с этой проблемой, используя версию 4.5.7.15008 Newtonsoft.Json. Я пробовал все предлагаемые здесь решения вместе с некоторыми другими. Я решил проблему, используя приведенный ниже код. В принципе, вы можете просто использовать другой 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

Я просто наткнулся на это сам, и я отрывал свои волосы от разочарования!

Чтобы решить эту проблему, для меня это работало, но поскольку я пропустил решение 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);
    }
}

Edit:

ОП указал, что использование 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);
    }
}

Ответ 4

У меня была такая же проблема с родительскими/детскими коллекциями, и я нашел эту должность, которая решила мое дело. Я только хотел показать список родительских элементов коллекции и не нуждался ни в каких дочерних данных, поэтому я использую следующее, и он работал нормально:

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

он также ссылается на страницу codplex Json.NET по адресу:

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

Ответ 5

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

Это может помочь кому-то, но в моем случае я пытался переопределить метод Equals, чтобы мой объект рассматривался как тип значения. В своем исследовании я обнаружил, что JSON.NET не нравится:

Ошибка саморекламы JSON.NET

Ответ 7

Моя была простой ошибкой и не имела никакого отношения к решению этой темы.

Этот раздел был первой страницей в google, поэтому я публикую здесь, если у других будет такая же проблема, как и у меня.

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