web-dev-qa-db-de.com

Selbstreferenzierende Schleife in Json.Net JsonSerializer aus benutzerdefiniertem JsonConverter (Web-API)

Das Projekt ist ein Asp.Net Web API-Webdienst.

Ich habe eine Typhierarchie, die ich in und aus Json serialisieren können muss. Daher habe ich den Code aus dieser SO entnommen: Wie kann man einen benutzerdefinierten JsonConverter in JSON.NET implementieren, um eine Liste der Basisklassenobjekte zu deserialisieren? und den Konverter auf die Basisklasse meiner Hierarchie angewendet; so etwas (hier ist Pseudo-Code, um irrelevante Ereignisse zu verbergen):

[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
{

}

Wenn also eine RootType-Instanz deserialisiert wird, deren BaseTypeMember gleich einer Instanz von DerivedType war, wird sie wieder in eine Instanz dieses Typs deserialisiert.

Für den Datensatz enthalten diese JSON-Objekte ein '$type'-Feld, das virtuelle Typnamen (nicht vollständige .Net-Typnamen) enthält, sodass ich gleichzeitig Typen im JSON unterstützen kann und gleichzeitig genau kontrollieren kann, welche Typen serialisiert und deserialisiert werden können.

Das funktioniert nun wirklich gut, um Werte aus der Anfrage zu deserialisieren. aber ich habe ein Problem mit der Serialisierung. Wenn Sie sich die verlinkte SO und die Json.Net-Diskussion ansehen, die in der obersten Antwort verlinkt ist, werden Sie feststellen, dass der Basiscode, den ich verwende, vollständig auf Deserialisierung ausgerichtet ist. Beispiele für seine Verwendung zeigen die manuelle Erstellung des Serializers. Die JsonConverter-Implementierung, die durch diesen JsonCreationConverter<T> in die Tabelle eingefügt wird, wirft einfach eine NotImplementedException aus.

Aufgrund der Art und Weise, wie das Web-API einen einzelnen Formatierer für eine Anfrage verwendet, muss ich die Standardserialisierung in der WriteObject-Methode implementieren.

Ich muss an dieser Stelle betonen, dass ich alles vor dem Start dieses Projektes ordnungsgemäß serialisiert hatte ohne Fehler.

Also habe ich das gemacht: 

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

Ich erhalte jedoch eine JsonSerializationException: Self referencing loop detected with type 'DerivedType', wenn eines der Objekte serialisiert wird. Nochmal - wenn ich das Converter-Attribut entferne (meine benutzerdefinierte Erstellung deaktivieren), funktioniert es gut ...

Ich habe das Gefühl, dass dies bedeutet, dass mein Serialisierungscode tatsächlich den Konverter erneut auf demselben Objekt auslöst, was wiederum den Serializer erneut aufruft - ad nauseam. Bestätigt - siehe meine Antwort

Also, welcher Code sollte ich schreibe in WriteObject, der dieselbe "Standard" -Serialisierung durchführt, die funktioniert?

36
Andras Zoltan

Nun, das hat Spaß gemacht ...

Als ich die Stack-Ablaufverfolgung für die Ausnahme genauer unter die Lupe nahm, fiel mir auf, dass die Methode JsonSerializerInternalWriter.SerializeConvertable zweimal vorhanden war. In der Tat war es diese Methode, die sich ganz oben im Stack befand - nämlich JsonSerializerInternalWriter.CheckForCircularReference -, wodurch wiederum die Ausnahme ausgelöst wurde. Es war jedoch auch die Quelle des Aufrufs der Write-Methode meines eigenen Konverters.

Es sieht also so aus, als würde der Serializer folgendes tun:

  • 1) Wenn das Objekt einen Konverter hat
    • 1a) Werfen, wenn Zirkelverweis
    • 1b) Rufen Sie die Write-Methode des Konverters auf
  • 2) Else
    • 2a) Verwenden Sie interne Serialisierer

In diesem Fall ruft das Json.Net meinen Konverter auf, der wiederum den Json.Net-Serialisierer anruft, der dann explodiert, weil er das Objekt, das ihm übergeben wurde, bereits serialisiert!

ILSpy auf der DLL öffnen (ja, ich weiß, es ist Open Source - aber ich möchte die 'Callers'-Funktionalität!) Und den Aufrufstack von SerializeConvertable auf JsonSerializerInternalWriter.SerializeValue hochschieben, der Code, der feststellt, ob ein Konverter verwendet werden soll gleich am Start gefunden:

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

Zum Glück bietet diese letzte Bedingung in der if-Anweisung die Lösung für mein Problem: Alles, was ich tun musste, war, entweder den Basis-Konverter hinzuzufügen, der aus dem Code in der verknüpften SO in der Frage oder in der abgeleiteter:

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

Und jetzt funktioniert alles gut.

Die Folge davon ist jedoch, dass, wenn Sie eine benutzerdefinierte JSON-Serialisierung für ein Objekt haben und Sie es mit einem Konverter und injizieren, Sie beabsichtigen, auf den Standardserialisierungsmechanismus unter zurückzugreifen einige oder alle Situationen; Dann können Sie das nicht tun, weil Sie den Rahmen dazu bringen, zu denken, Sie versuchen, einen Zirkelverweis zu speichern.

Ich habe zwar versucht, das Member ReferenceLoopHandling zu manipulieren, aber wenn ich ihm Ignore sagte, wurde nichts serialisiert, und wenn ich ihm sagte, er solle sie retten, überrascht es nicht, dass ich einen Stack-Überlauf bekam.

Es ist möglich, dass dies ein Fehler in Json.Net ist - in Ordnung, es ist so viel ein Randfall, dass die Gefahr besteht, dass man vom Rand des Universums herunterfällt - aber wenn man sich in dieser Situation befindet, bleibt man irgendwie hängen !

52
Andras Zoltan

Dieses Problem ist bei Verwendung der Version 4.5.7.15008 von Newtonsoft.Json aufgetreten. Ich habe alle hier angebotenen Lösungen zusammen mit einigen anderen ausprobiert. Ich habe das Problem mit dem folgenden Code behoben. Grundsätzlich können Sie einfach einen anderen JsonSerializer verwenden, um die Serialisierung durchzuführen. Der JsonSerializer, der erstellt wird, hat keine registrierten Konverter, so dass der Wiedereinstieg/die Ausnahme vermieden wird. Wenn andere Einstellungen oder ContractResolver verwendet werden, müssen sie manuell für die erstellte serialisierte Einstellung festgelegt werden. Einige Konstruktorargumente könnten der CustomConverter-Klasse hinzugefügt werden, um dies zu berücksichtigen.

    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);
        }
    }
6
Ian Gibson

Ich hatte gerade das gleiche Problem mit den Eltern/Kind-Sammlungen und fand den Beitrag, der meinen Fall gelöst hat. Ich wollte nur die Liste der übergeordneten Sammlungselemente anzeigen und benötigte keine der untergeordneten Daten. Daher verwende ich Folgendes und es hat gut funktioniert:

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

es verweist auch auf die Json.NET-Codplex-Seite unter:

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

Ich bin gerade auf dieses Thema gestoßen und habe frustriert meine Haare ausgezogen!

Um das Problem zu lösen, funktionierte Folgendes für mich, aber da ich die Lösung CanWrite verpasst habe, ist dies eine komplexere Problemumgehung.

  • Erstellen Sie eine Kopie der vorhandenen Klasse, in der Sie Ihren Konverter verwenden, und nennen Sie es etwas anderes.
  • Entfernen Sie das JsonConverter-Attribut der Kopie.
  • Erstellen Sie einen Konstruktor für die neue Klasse, für den ein Parameter desselben Typs wie für die ursprüngliche Klasse verwendet wird. Verwenden Sie den Konstruktor, um alle Werte zu übernehmen, die für eine spätere Serialisierung erforderlich sind.
  • Konvertieren Sie in der WriteJson-Methode Ihres Konverters den Wert in Ihren Dummy-Typ und serialisieren Sie stattdessen diesen Typ.

Dies ist zum Beispiel meiner ursprünglichen Klasse ähnlich:

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

Die Kopie sieht so aus:

public class FakeMyResponse
{
    public ResponseBlog blog { get; set; }
    public Post[] posts { get; set; }

    public FakeMyResponse(MyResponse response)
    {
        blog = response.blog;
        posts = response.posts;
    }
}

Der WriteJson ist:

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

Bearbeiten:

Das OP wies darauf hin, dass die Verwendung eines Expando eine andere mögliche Lösung sein könnte. Dies funktioniert gut und erspart das Erstellen der neuen Klasse, obwohl für die DLR-Unterstützung Framework 4.0 oder höher erforderlich ist. Der Ansatz besteht darin, eine neue dynamicExpandoObject zu erstellen und dann ihre Eigenschaften in der WriteJson-Methode direkt zu initialisieren, um die Kopie zu erstellen, z.

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);
    }
}
2
Dave R.

IMO, das ist eine gravierende Einschränkung der Bibliothek. Die Lösung ist ziemlich einfach, obwohl ich zugeben muss, dass es nicht so schnell zu mir kam. Die Lösung ist zu setzen:

.ReferenceLoopHandling = ReferenceLoopHandling.Serialize

dies wird, wie überall dokumentiert, den selbstreferenzierenden Fehler beseitigen und durch einen Stapelüberlauf ersetzen. In meinem Fall benötigte ich Schreibfunktionalität. Daher war es nicht möglich, CanWrite auf false zu setzen. Am Ende habe ich nur ein Flag gesetzt, um den CanConvert-Aufruf zu schützen, wenn ich weiß, dass ein Anruf beim Serialisierer eine (endlose) Rekursion verursacht:

    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
1
2stroke

Dies mag jemandem helfen, aber in meinem Fall habe ich versucht, die Equals-Methode zu überschreiben, damit mein Objekt als Werttyp behandelt wird. In meiner Recherche habe ich festgestellt, dass JSON.NET das nicht mag:

JSON.NET-selbstreferenzierender Fehler

0
ProVega

Meiner war ein einfacher Fehler und hatte nichts mit der Lösung dieses Themas zu tun.

Dieses Thema war die erste Seite bei Google. Ich werde hier posten, falls andere das gleiche Problem hätten wie ich.

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