Json.NET, comment personnaliser la sérialisation pour insérer une propriété JSON

Je n’ai pas pu trouver d’implémentation raisonnable pour JsonConvert.WriteJson qui me permette d’insérer une propriété JSON lors de la sérialisation de types spécifiques. Toutes mes tentatives ont abouti à “JsonSerializationException: boucle d’auto-référencement détectée avec le type XXX”.

Un peu plus de fond sur le problème que je tente de résoudre: j’utilise JSON comme format de fichier de configuration et JsonConverter pour contrôler la résolution de type, la sérialisation et la désérialisation de mes types de configuration. Au lieu d’utiliser la propriété $type , je souhaite utiliser des valeurs JSON plus significatives utilisées pour résoudre les types corrects.

Dans mon exemple réduit, voici du texte JSON:

 { "Target": "B", "Id": "foo" } 

où la propriété JSON "Target": "B" est utilisée pour déterminer que cet object doit être sérialisé en type B Cette conception peut sembler peu convaincante compte tenu du simple exemple, mais elle rend le format du fichier de configuration plus utilisable.

Je veux aussi que les fichiers de configuration soient sortingples. Le cas de désérialisation fonctionne, ce que je ne peux pas obtenir, c’est le cas de sérialisation.

La racine de mon problème est que je ne peux pas trouver une implémentation de JsonConverter.WriteJson qui utilise la logique de sérialisation JSON standard et ne JsonConverter.WriteJson pas une exception “Auto referencing loop”. Voici ma mise en œuvre:

 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JProperty typeHintProperty = TypeHintPropertyForType(value.GetType()); //BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''. // Same error occurs whether I use the serializer parameter or a separate serializer. JObject jo = JObject.FromObject(value, serializer); if (typeHintProperty != null) { jo.AddFirst(typeHintProperty); } writer.WriteToken(jo.CreateReader()); } 

Cela me semble être un bogue dans Json.NET, car il devrait y avoir un moyen de le faire. Malheureusement, tous les exemples de JsonConverter.WriteJson que j’ai rencontrés (par exemple, la conversion personnalisée d’objects spécifiques dans JSON.NET ) fournissent uniquement la sérialisation personnalisée d’une classe spécifique, à l’aide des méthodes JsonWriter pour écrire des objects et des propriétés individuels.

Voici le code complet d’un test xunit qui expose mon problème (ou le voir ici )

 using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using Xunit; public class A { public ssortingng Id { get; set; } public A Child { get; set; } } public class B : A {} public class C : A {} ///  /// Shows the problem I'm having serializing classes with Json. ///  public sealed class JsonTypeConverterProblem { [Fact] public void ShowSerializationBug() { A a = new B() { Id = "foo", Child = new C() { Id = "bar" } }; JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); jsonSettings.ContractResolver = new TypeHintContractResolver(); ssortingng json = JsonConvert.SerializeObject(a, Formatting.Indented, jsonSettings); Console.WriteLine(json); Assert.Contains(@"""Target"": ""B""", json); Assert.Contains(@"""Is"": ""C""", json); } [Fact] public void DeserializationWorks() { ssortingng json = @"{ ""Target"": ""B"", ""Id"": ""foo"", ""Child"": { ""Is"": ""C"", ""Id"": ""bar"", } }"; JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); jsonSettings.ContractResolver = new TypeHintContractResolver(); A a = JsonConvert.DeserializeObject(json, jsonSettings); Assert.IsType(a); Assert.IsType(a.Child); } } public class TypeHintContractResolver : DefaultContractResolver { public override JsonContract ResolveContract(Type type) { JsonContract contract = base.ResolveContract(type); if ((contract is JsonObjectContract) && ((type == typeof(A)) || (type == typeof(B))) ) // In the real implementation, this is checking against a registry of types { contract.Converter = new TypeHintJsonConverter(type); } return contract; } } public class TypeHintJsonConverter : JsonConverter { private readonly Type _declaredType; public TypeHintJsonConverter(Type declaredType) { _declaredType = declaredType; } public override bool CanConvert(Type objectType) { return objectType == _declaredType; } // The real implementation of the next 2 methods uses reflection on concrete types to determine the declaredType hint. // TypeFromTypeHint and TypeHintPropertyForType are the inverse of each other. private Type TypeFromTypeHint(JObject jo) { if (new JValue("B").Equals(jo["Target"])) { return typeof(B); } else if (new JValue("A").Equals(jo["Hint"])) { return typeof(A); } else if (new JValue("C").Equals(jo["Is"])) { return typeof(C); } else { throw new ArgumentException("Type not recognized from JSON"); } } private JProperty TypeHintPropertyForType(Type type) { if (type == typeof(A)) { return new JProperty("Hint", "A"); } else if (type == typeof(B)) { return new JProperty("Target", "B"); } else if (type == typeof(C)) { return new JProperty("Is", "C"); } else { return null; } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (! CanConvert(objectType)) { throw new InvalidOperationException("Can't convert declaredType " + objectType + "; expected " + _declaredType); } // Load JObject from stream. Turns out we're also called for null arrays of our objects, // so handle a null by returning one. var jToken = JToken.Load(reader); if (jToken.Type == JTokenType.Null) return null; if (jToken.Type != JTokenType.Object) { throw new InvalidOperationException("Json: expected " + _declaredType + "; got " + jToken.Type); } JObject jObject = (JObject) jToken; // Select the declaredType based on TypeHint Type deserializingType = TypeFromTypeHint(jObject); var target = Activator.CreateInstance(deserializingType); serializer.Populate(jObject.CreateReader(), target); return target; } public override bool CanWrite { get { return true; } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JProperty typeHintProperty = TypeHintPropertyForType(value.GetType()); //BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''. // Same error occurs whether I use the serializer parameter or a separate serializer. JObject jo = JObject.FromObject(value, serializer); if (typeHintProperty != null) { jo.AddFirst(typeHintProperty); } writer.WriteToken(jo.CreateReader()); } } 

L’appel de JObject.FromObject() depuis un convertisseur sur le même object converti entraînera une boucle récursive, comme vous l’avez vu. Normalement, la solution consiste à (a) utiliser une instance distincte de JsonSerializer dans le convertisseur ou à (b) sérialiser les propriétés manuellement, comme l’a souligné James dans sa réponse. Votre cas est un peu spécial en ce sens qu’aucune de ces solutions ne fonctionne vraiment pour vous: si vous utilisez une instance de sérialiseur distincte qui ne connaît pas le convertisseur, vos objects enfant ne verront pas leurs propriétés d’indice appliquées. Et la sérialisation entièrement manuelle ne fonctionne pas pour une solution généralisée, comme vous l’avez mentionné dans vos commentaires.

Heureusement, il y a un terrain d’entente. Vous pouvez utiliser un peu de reflection dans votre méthode WriteJson pour obtenir les propriétés de l’object, puis en déléguer à JToken.FromObject() . Le convertisseur sera appelé de manière récursive, comme il se doit pour les propriétés enfants, mais pas pour l’object en cours, afin d’éviter tout problème. Un inconvénient avec cette solution: si vous atsortingbuez des atsortingbuts [JsonProperty] aux classes gérées par ce convertisseur (A, B et C dans votre exemple), ces atsortingbuts ne seront pas respectés.

Voici le code mis à jour pour la méthode WriteJson :

 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JProperty typeHintProperty = TypeHintPropertyForType(value.GetType()); JObject jo = new JObject(); if (typeHintProperty != null) { jo.Add(typeHintProperty); } foreach (PropertyInfo prop in value.GetType().GetProperties()) { if (prop.CanRead) { object propValue = prop.GetValue(value); if (propValue != null) { jo.Add(prop.Name, JToken.FromObject(propValue, serializer)); } } } jo.WriteTo(writer); } 

Violon: https://dotnetfiddle.net/jQrxb8

Exemple d’utilisation d’un convertisseur personnalisé pour prendre une propriété que nous ignorons, la décomposer et l’append à son object parent:

 public class ContextBaseSerializer : JsonConverter { public override bool CanConvert(Type objectType) { return typeof(ContextBase).GetTypeInfo().IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var contextBase = value as ContextBase; var valueToken = JToken.FromObject(value, new ForcedObjectSerializer()); if (contextBase.Properties != null) { var propertiesToken = JToken.FromObject(contextBase.Properties); foreach (var property in propertiesToken.Children()) { valueToken[property.Name] = property.Value; } } valueToken.WriteTo(writer); } } 

Nous devons remplacer le sérialiseur afin de pouvoir spécifier un résolveur personnalisé:

 public class ForcedObjectSerializer : JsonSerializer { public ForcedObjectSerializer() : base() { this.ContractResolver = new ForcedObjectResolver(); } } 

Et dans le résolveur personnalisé, nous allons supprimer le convertisseur de JsonContract, cela forcera les sérialiseurs internes à utiliser le sérialiseur d’object par défaut:

 public class ForcedObjectResolver : DefaultContractResolver { public override JsonContract ResolveContract(Type type) { // We're going to null the converter to force it to serialize this as a plain object. var contract = base.ResolveContract(type); contract.Converter = null; return contract; } } 

Cela devrait vous amener là, ou assez proche. 🙂 J’utilise ceci dans https://github.com/RoushTech/SegmentDotNet/ qui contient des scénarios de test couvrant ce cas d’utilisation (y compris l’imbrication de notre classe sérialisée personnalisée), des détails sur la discussion couvrant ici: https://github.com /JamesNK/Newtonsoft.Json/issues/386

Que dis-tu de ça:

 public class TypeHintContractResolver : DefaultContractResolver { protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { IList result = base.CreateProperties(type, memberSerialization); if (type == typeof(A)) { result.Add(CreateTypeHintProperty(type,"Hint", "A")); } else if (type == typeof(B)) { result.Add(CreateTypeHintProperty(type,"Target", "B")); } else if (type == typeof(C)) { result.Add(CreateTypeHintProperty(type,"Is", "C")); } return result; } private JsonProperty CreateTypeHintProperty(Type declaringType, ssortingng propertyName, ssortingng propertyValue) { return new JsonProperty { PropertyType = typeof (ssortingng), DeclaringType = declaringType, PropertyName = propertyName, ValueProvider = new TypeHintValueProvider(propertyValue), Readable = false, Writable = true }; } } 

Le fournisseur de valeur de type requirejs pour cela peut être aussi simple que cela:

 public class TypeHintValueProvider : IValueProvider { private readonly ssortingng _value; public TypeHintValueProvider(ssortingng value) { _value = value; } public void SetValue(object target, object value) { } public object GetValue(object target) { return _value; } } 

Violon: https://dotnetfiddle.net/DRNzz8

Le sérialiseur appelle votre convertisseur qui appelle ensuite le sérialiseur appelant votre convertisseur, etc.

Utilisez une nouvelle instance du sérialiseur dont le convertisseur n’est pas associé à JObject.FromObject ou sérialisez les membres du type manuellement.

J’ai eu un problème similaire et voici ce que je fais dans le résolveur de contrat

 if (contract is JsonObjectContract && ShouldUseConverter(type)) { if (contract.Converter is TypeHintJsonConverter) { contract.Converter = null; } else { contract.Converter = new TypeHintJsonConverter(type); } } 

C’est le seul moyen que j’ai trouvé d’éviter l’exception StackOverflowException. En réalité, aucun autre appel n’utilisera le convertisseur.

La réponse de Brian est excellente et devrait aider le PO, mais elle comporte quelques problèmes que d’autres peuvent rencontrer, à savoir: 1) une exception de débordement est générée lors de la sérialisation des propriétés d’un tableau, 2) toutes les propriétés publiques statiques seront émises vers JSON qui vous ne voulez probablement pas.

Voici une autre version qui s’attaque à ces problèmes:

 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { Type valueType = value.GetType(); if (valueType.IsArray) { var jArray = new JArray(); foreach (var item in (IEnumerable)value) jArray.Add(JToken.FromObject(item, serializer)); jArray.WriteTo(writer); } else { JProperty typeHintProperty = TypeHintPropertyForType(value.GetType()); var jObj = new JObject(); if (typeHintProperty != null) jo.Add(typeHintProperty); foreach (PropertyInfo property in valueType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (property.CanRead) { object propertyValue = property.GetValue(value); if (propertyValue != null) jObj.Add(property.Name, JToken.FromObject(propertyValue, serializer)); } } jObj.WriteTo(writer); } } 

Après avoir eu le même problème, et avoir trouvé ceci et d’autres questions similaires, j’ai constaté que le JsonConverter avait une propriété CanWrite surabondable.

Redéfinir cette propriété pour renvoyer false a résolu ce problème pour moi.

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

J’espère que cela aidera d’autres personnes ayant le même problème.