2014-09-30 7 views
12

Non sono riuscito a trovare un'implementazione ragionevole per JsonConvert.WriteJson che mi consente di inserire una proprietà JSON durante la serializzazione di tipi specifici. Tutti i miei tentativi hanno portato a "JsonSerializationException: loop autoreferenziale rilevato con tipo XXX".Json.NET, come personalizzare la serializzazione per inserire una proprietà JSON

Un po 'più di background sul problema che sto cercando di risolvere: sto usando JSON come formato di file di configurazione e sto usando un JsonConverter per controllare la risoluzione del tipo, la serializzazione e la deserializzazione dei miei tipi di configurazione. Invece di utilizzare la proprietà $type, desidero utilizzare valori JSON più significativi utilizzati per risolvere i tipi corretti.

Nel mio esempio pared-down, ecco qualche testo JSON:

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

in cui la proprietà JSON "Target": "B" viene utilizzato per determinare che questo oggetto deve essere serializzato in tipo B. Questo disegno potrebbe non sembrare così convincente dato il semplice esempio, ma rende il formato del file di configurazione più utilizzabile.

Desidero anche che i file di configurazione siano rotondi. Ho il caso di deserializzazione in funzione, quello che non riesco a far funzionare è il caso di serializzazione.

La radice del mio problema è che non riesco a trovare un'implementazione di JsonConverter.WriteJson che utilizza la logica di serializzazione JSON standard e non genera un'eccezione "Loop di riferimento automatico". Ecco la mia realizzazione:

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

La mi sembra essere un bug nel Json.NET, perché ci dovrebbe essere un modo per fare questo. Sfortunatamente tutti gli esempi di JsonConverter.WriteJson che ho trovato (ad esempio Custom conversion of specific objects in JSON.NET) forniscono solo la serializzazione personalizzata di una classe specifica, utilizzando i metodi JsonWriter per scrivere singoli oggetti e proprietà.

Ecco il codice completo per un test xUnit che presenta il mio problema (o see it here)

using System; 

using Newtonsoft.Json; 
using Newtonsoft.Json.Linq; 
using Newtonsoft.Json.Serialization; 

using Xunit; 


public class A 
{ 
    public string Id { get; set; } 
    public A Child { get; set; } 
} 

public class B : A {} 

public class C : A {} 

/// <summary> 
/// Shows the problem I'm having serializing classes with Json. 
/// </summary> 
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(); 
     string json = JsonConvert.SerializeObject(a, Formatting.Indented, jsonSettings); 
     Console.WriteLine(json); 

     Assert.Contains(@"""Target"": ""B""", json); 
     Assert.Contains(@"""Is"": ""C""", json); 
    } 

    [Fact] 
    public void DeserializationWorks() 
    { 
     string json = 
@"{ 
    ""Target"": ""B"", 
    ""Id"": ""foo"", 
    ""Child"": { 
     ""Is"": ""C"", 
     ""Id"": ""bar"", 
    } 
}"; 

     JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); 
     jsonSettings.ContractResolver = new TypeHintContractResolver(); 
     A a = JsonConvert.DeserializeObject<A>(json, jsonSettings); 

     Assert.IsType<B>(a); 
     Assert.IsType<C>(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()); 
    } 

} 
+1

Nel tuo metodo 'WriteJson' nel tuo convertitore, hai provato a rimuovere il parametro 'serializer' da' JObject.FromObject() chiamata del tutto? Sembra funzionare in [questo violino] (https://dotnetfiddle.net/lZfCWJ) –

+0

Grazie Brian - grazie per aver guardato questo, e hai ragione, che corregge l'Eccezione. Tuttavia, non risolve il mio problema, perché devo essere in grado di farlo in oggetti nidificati. Ho aggiornato l'esempio per coprirlo. Oppure, vedi https://dotnetfiddle.net/b3yrEU (Fiddle è COOL !!) – crimbo

+1

Sarei interessato a sapere con cosa sei finito. Sto avendo lo stesso problema. –

risposta

1

Il serializzatore sta chiamando nel vostro convertitore che viene poi mette in serializzatore, che sta chiamando nel vostro convertitore, ecc

Utilizzare una nuova istanza del serializzatore che non ha il convertitore con JObject.FromObject o serializzare manualmente i membri del tipo.

+1

Grazie. La serializzazione manuale dei membri del mio tipo non è praticabile, in quanto si tratta di un problema generalizzato e ho bisogno che funzioni con qualsiasi tipo configurato. Quello che sto cercando è un modo per intercettare la normale logica di serializzazione per inserire la proprietà. Idealmente, userebbe anche qualsiasi altra impostazione di serializzazione personalizzata nel serializzatore originale, ma per ora posso farcela senza. Lo proverò utilizzando un secondo serializzatore e JObject. – crimbo

9

Chiamare JObject.FromObject() dall'interno di un convertitore sullo stesso oggetto da convertire darà come risultato un ciclo ricorsivo, come si è visto. Normalmente la soluzione è quella di (a) utilizzare un'istanza di JsonSerializer separata all'interno del convertitore, oppure (b) serializzare le proprietà manualmente, come James ha indicato nella sua risposta. Il tuo caso è un po 'speciale nel fatto che nessuna di queste soluzioni funziona davvero per te: se usi un'istanza serializzatore separata che non conosce il convertitore, gli oggetti figlio non riceveranno le loro proprietà di suggerimento applicate. E la serializzazione completamente manuale non funziona per una soluzione generalizzata, come hai detto nei tuoi commenti.

Fortunatamente, c'è una via di mezzo. È possibile utilizzare un po 'di riflessione nel metodo WriteJson per ottenere le proprietà dell'oggetto, quindi delegare da lì a JToken.FromObject(). Il convertitore verrà chiamato in modo ricorsivo come dovrebbe per le proprietà figlio, ma non per l'oggetto corrente, in modo da non finire nei guai.Un avvertimento con questa soluzione: se si dispone di eventuali attributi [JsonProperty] applicati alle classi gestite da questo convertitore (A, B e C nell'esempio), tali attributi non verranno rispettati.

Ecco il codice aggiornato per il metodo 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); 
} 

Fiddle: https://dotnetfiddle.net/jQrxb8

1

Ho avuto un problema simile e qui è quello che faccio nel resolver contratto di

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

Questo è stato l'unico modo in cui ho trovato per evitare StackOverflowException. In effetti ogni altra chiamata non utilizzerà il convertitore.

-2

Dopo aver riscontrato lo stesso problema e aver trovato questa e altre domande simili, ho trovato che lo JsonConverter dispone di una proprietà CanWrite esagerata.

Sostituire questa proprietà per restituire falso risolto questo problema per me.

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

Speriamo che questo aiuti gli altri ad avere lo stesso problema.

+0

Facendo questo tramite una soluzione leggermente modificata da http://stackoverflow.com/a/9444519/1037948, ho iniziato a ricevere 'NotImplementedException' da' WriteJson' di recente, e non ho idea del perché. Sospetto che sia perché ho anche annullato "CanConvert". Volevo solo sottolineare. – drzaus

+3

Se 'CanWrite' restituisce false' WriteJson' non viene chiamato affatto. –

3

ne dite di questo:

public class TypeHintContractResolver : DefaultContractResolver 
{ 

    protected override IList<JsonProperty> CreateProperties(Type type, 
     MemberSerialization memberSerialization) 
    { 
    IList<JsonProperty> 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, string propertyName, string propertyValue) 
    { 
    return new JsonProperty 
    { 
     PropertyType = typeof (string), 
     DeclaringType = declaringType, 
     PropertyName = propertyName, 
     ValueProvider = new TypeHintValueProvider(propertyValue), 
     Readable = false, 
     Writable = true 
    }; 
    } 
} 

Il fornitore di valore del tipo richiesto per che può essere semplice come questo:

public class TypeHintValueProvider : IValueProvider 
{ 

    private readonly string _value; 
    public TypeHintValueProvider(string value) 
    { 
    _value = value; 
    } 

    public void SetValue(object target, object value) 
    {   
    } 

    public object GetValue(object target) 
    { 
    return _value; 
    } 

} 

Fiddle: https://dotnetfiddle.net/DRNzz8

+0

Questo sembra come dopo, ma voglio usarlo per la serializzazione ma non riesco a farlo funzionare (le proprietà vengono aggiunte ma non appaiono in json sul browser). Dovrebbe funzionare per la serializzazione? –

1

risposta di Brian è grande e dovrebbe aiutare l'OP, ma la risposta ha un paio di problemi che altri potrebbero incontrare, in particolare: 1) un'eccezione di overflow viene generata durante la serializzazione delle proprietà dell'array, 2) qualsiasi statico le proprietà pubbliche verranno emesse a JSON che probabilmente non si desidera.

Ecco un'altra versione che affronta questi problemi:

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

Esempio di utilizzo di un convertitore personalizzato per prendere una proprietà ignoriamo, scomposizione e aggiungere le sue proprietà al suo oggetto principale .:

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<JProperty>()) 
      { 
       valueToken[property.Name] = property.Value; 
      } 
     } 

     valueToken.WriteTo(writer); 
    } 
} 

Dobbiamo ignorare il serializzatore in modo che possiamo specificare un resolver personalizzato:

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

E nella risoluzione personalizzato faremo trash il convertitore dal JsonContract, questo costringerà i serializzatori interne per utilizzare il serializzatore oggetto predefinito:

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

Questo dovrebbe arrivare lì, o abbastanza vicino. :) Io uso questo in https://github.com/RoushTech/SegmentDotNet/ che ha casi di test che coprono questo caso d'uso (compreso l'annidamento della nostra classe serializzata personalizzata), dettagli sulla discussione che riguardano questo: https://github.com/JamesNK/Newtonsoft.Json/issues/386

+1

Questa è facilmente la risposta più sottovalutata qui. Per quello che vale, questa non è stata una soluzione perfetta al 100% per me, dato che voglio davvero usare tutte le impostazioni del serializzatore originale. [Leggi questa risposta] (http://stackoverflow.com/a/38230327/2283050). Potresti considerare di perfezionare questa risposta per riflettere i benefici. Comunque, lavoro fantastico. –

Problemi correlati