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());
}
}
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) –
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
Sarei interessato a sapere con cosa sei finito. Sto avendo lo stesso problema. –