web-dev-qa-db-fra.com

Boucle de référence automatique dans Json.Net JsonSerializer à partir de JsonConverter personnalisé (API Web)

Le projet est un service Web API Web Asp.Net.

J'ai une hiérarchie de types dont j'ai besoin pour pouvoir sérialiser à et de Json, donc j'ai pris le code de ce SO: Comment implémenter JsonConverter personnalisé dans JSON.NET pour désérialiser une liste d'objets de classe de base? , et a appliqué le convertisseur à la classe de base de ma hiérarchie; quelque chose comme ceci (il y a un pseudo-code ici pour cacher les irrégularités):

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

}

Donc, si je désérialise une instance RootType dont BaseTypeMember était égal à une instance de DerivedType, elle sera alors désérialisée dans une instance de ce type.

Pour l'enregistrement, ces objets JSON contiennent un champ '$type' qui contient des noms de types virtuels (et non des noms de types .Net complets) afin que je puisse simultanément prendre en charge des types dans JSON tout en contrôlant exactement quels types peuvent être sérialisés et désérialisés.

Maintenant, cela fonctionne vraiment bien pour désérialiser les valeurs de la requête; mais j'ai un problème avec la sérialisation. Si vous examinez le SO lié et la discussion Json.Net qui est liée à la réponse principale, vous verrez que le code de base que j'utilise est entièrement conçu pour la désérialisation; avec des exemples de son utilisation montrant la création manuelle du sérialiseur. L'implémentation JsonConverter apportée à la table par ce JsonCreationConverter<T> lève simplement un NotImplementedException.

Maintenant, à cause de la manière dont l'API Web utilise un seul formateur pour une demande, je dois implémenter une sérialisation "standard" dans la méthode WriteObject.

À ce stade, je dois souligner qu’avant de me lancer dans cette partie de mon projet, j’avais tout sérialisé correctement sans erreur.

Alors j'ai fait ça: 

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

Mais je reçois une JsonSerializationException: Self referencing loop detected with type 'DerivedType', lorsque l’un des objets est sérialisé. Encore une fois - si je supprime l'attribut du convertisseur (en désactivant ma création personnalisée), alors cela fonctionne correctement ...

J'ai l'impression que cela signifie que mon code de sérialisation déclenche de nouveau le convertisseur sur le même objet, qui à son tour appelle à nouveau le sérialiseur - ad nauseam. Confirmé - voir ma réponse

Alors quel code devrait j'écris dans WriteObject qui fera la même sérialisation 'standard' qui fonctionne?

36
Andras Zoltan

C'était amusant ...

Quand j'ai regardé de plus près la trace de la pile pour l'exception, j'ai remarqué que la méthode JsonSerializerInternalWriter.SerializeConvertable y figurait deux fois. En fait, c'était cette méthode qui se trouvait au sommet de la pile - appelant JsonSerializerInternalWriter.CheckForCircularReference - qui lançait à son tour l'exception. C'était aussi, cependant, la source de l'appel à la méthode Write de mon propre convertisseur.

Donc, il semblerait que le sérialiseur faisait:

  • 1) Si l'objet a un convertisseur
    • 1a) Lancer si référence circulaire
    • 1b) Invoquer la méthode d'écriture du convertisseur
  • 2) Sinon
    • 2a) Utiliser des sérialiseurs internes

Donc, dans ce cas, Json.Net appelle mon convertisseur qui à son tour appelle le sérialiseur Json.Net qui explose ensuite car il voit que l'objet en cours de sérialisation lui a déjà été transmis!

Ouvrir ILSpy sur DLL (oui, je sais que c'est open source - mais je veux la fonctionnalité "appelants"!) Et déplacer la pile d'appels de SerializeConvertable à JsonSerializerInternalWriter.SerializeValue, le code qui détecte si un convertisseur doit être utilisé peut être trouvé juste au début:

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

Heureusement, cette toute dernière condition de l'instruction if fournit la solution à mon problème: tout ce que je devais faire était d'ajouter ce qui suit au convertisseur de base copié à partir du code contenu dans le code SO lié dans la question ou un dérivé:

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

Et maintenant tout fonctionne bien.

Toutefois, si vous avez l’intention d’avoir une sérialisation JSON personnalisée sur un objet et que vous l’injectez avec un convertisseuret, vous avez l’intention de revenir au mécanisme de sérialisation standard sous toutes les situations; alors vous ne pouvez pas, car vous allez tromper le cadre en pensant que vous essayez de stocker une référence circulaire.

J'ai essayé de manipuler le membre ReferenceLoopHandling, mais si je lui ai donné Ignore, alors rien n'a été sérialisé et si je lui ai dit de les sauvegarder, sans surprise, j'ai un débordement de pile.

Il est possible qu'il s'agisse d'un bogue dans Json.Net - d'accord, c'est tellement un cas Edge qu'il risque de tomber du bord de l'univers - mais si vous vous trouvez dans cette situation, vous êtes un peu coincé. !

52
Andras Zoltan

J'ai rencontré ce problème avec la version 4.5.7.15008 de Newtonsoft.Json. J'ai essayé toutes les solutions proposées ici avec d'autres. J'ai résolu le problème en utilisant le code ci-dessous. En gros, vous pouvez simplement utiliser un autre JsonSerializer pour effectuer la sérialisation. Le JsonSerializer créé n'a pas de convertisseurs enregistrés, donc la ré-entrée/exception sera évitée. Si d'autres paramètres ou ContractResolver sont utilisés, ils devront les définir manuellement sur le sérialisé créé: certains arguments de constructeur peuvent être ajoutés à la classe CustomConverter pour l'adapter.

    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

Je viens d'avoir le même problème avec les collections parent/enfant et j'ai trouvé ce post qui a résolu mon cas. Je voulais seulement afficher la liste des éléments de la collection parente et je n'ai besoin d'aucune donnée enfant. Par conséquent, j'utilise les éléments suivants et tout a bien fonctionné:

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

il fait également référence à la page du codplex Json.NET à l'adresse:

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

Je viens de découvrir cela moi-même et je me tirais les cheveux de frustration!

Pour résoudre le problème, les éléments suivants ont fonctionné pour moi, mais comme j'ai raté la solution CanWrite, la solution de contournement est plus complexe.

  • Créez une copie de la classe existante sur laquelle vous utilisez votre convertisseur et appelez-la autrement.
  • Supprimez l'attribut JsonConverter sur la copie.
  • Créez un constructeur sur la nouvelle classe qui prend un paramètre du même type que la classe d'origine. Utilisez le constructeur pour copier les valeurs requises pour une sérialisation ultérieure.
  • Dans la méthode WriteJson de votre convertisseur, convertissez la valeur en votre type factice, puis sérialisez ce type à la place.

Par exemple, cela ressemble à ma classe d'origine:

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

La copie ressemble à ceci:

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

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

WriteJson est:

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

Modifier:

Le PO a souligné que l'utilisation d'un Expando pourrait être une autre solution possible. Cela fonctionne bien, évitant ainsi la création de la nouvelle classe, bien que la prise en charge de DLR nécessite Framework 4.0 ou une version ultérieure. L’approche consiste à créer une nouvelle variable dynamicExpandoObject, puis à initialiser ses propriétés dans la méthode WriteJson directement pour créer la copie, par exemple:

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.

OMI, ceci est une limitation sérieuse de la bibliothèque. La solution est assez simple, même si je dois admettre que cela ne m'est pas venu si vite. La solution consiste à définir:

.ReferenceLoopHandling = ReferenceLoopHandling.Serialize

qui, comme indiqué partout, éliminera l'erreur d'auto-référencement et la remplacera par un débordement de pile. Dans mon cas, j'avais besoin de la fonctionnalité d'écriture. Par conséquent, définir CanWrite sur false n'était pas une option. En fin de compte, je viens de définir un drapeau pour garder l'appel CanConvert lorsque je sais qu'un appel au sérialiseur est en train de provoquer une récursion (sans fin):

    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

Cela pourrait aider quelqu'un, mais dans mon cas, j'essayais de remplacer la méthode Equals pour que mon objet soit traité comme type de valeur. Dans mes recherches, j'ai trouvé que JSON.NET n'aimait pas ça:

Erreur d'auto-référencement JSON.NET

0
ProVega

La mienne était une simple erreur et n'avait rien à voir avec la solution de ce sujet.

Ce sujet était la 1ère page de Google. Je poste ici au cas où d’autres auraient le même problème que moi.

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