JSON.Net arroja StackOverflowException cuando usa

Escribí este código simple para serializar clases como aplanar, pero cuando uso la [JsonConverter(typeof(FJson))] , arroja una StackOverflowException . Si llamo al SerializeObject manualmente, funciona bien.

¿Cómo puedo usar JsonConvert en el modo Anotación?

 class Program { static void Main(string[] args) { A a = new A(); a.id = 1; abname = "value"; string json = null; // json = JsonConvert.SerializeObject(a, new FJson()); without [JsonConverter(typeof(FJson))] annotation workd fine // json = JsonConvert.SerializeObject(a); StackOverflowException Console.WriteLine(json); Console.ReadLine(); } } //[JsonConverter(typeof(FJson))] StackOverflowException public class A { public A() { this.b = new B(); } public int id { get; set; } public string name { get; set; } public B b { get; set; } } public class B { public string name { get; set; } } public class FJson : JsonConverter { public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JToken t = JToken.FromObject(value); if (t.Type != JTokenType.Object) { t.WriteTo(writer); return; } JObject o = (JObject)t; writer.WriteStartObject(); WriteJson(writer, o); writer.WriteEndObject(); } private void WriteJson(JsonWriter writer, JObject value) { foreach (var p in value.Properties()) { if (p.Value is JObject) WriteJson(writer, (JObject)p.Value); else p.WriteTo(writer); } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override bool CanConvert(Type objectType) { return true; // works for any type } } 

Json.NET no tiene soporte conveniente para los convertidores que llaman a JToken.FromObject para generar una serialización “predeterminada”, luego modifican el JToken resultante para la salida, precisamente porque se producirá la StackOverflowException que ha observado.

Una solución es desactivar temporalmente el convertidor en llamadas recursivas utilizando un booleano estático de subprocesos. Se usa un hilo estático porque, en algunas situaciones, incluida asp.net-web-api , las instancias de los convertidores JSON se compartirán entre hilos. En ese caso, deshabilitar el convertidor a través de una propiedad de instancia no será seguro para subprocesos.

 public class FJson : JsonConverter { [ThreadStatic] static bool disabled; // Disables the converter in a thread-safe manner. bool Disabled { get { return disabled; } set { disabled = value; } } public override bool CanWrite { get { return !Disabled; } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JToken t; using (new PushValue(true, () => Disabled, (canWrite) => Disabled = canWrite)) { t = JToken.FromObject(value, serializer); } if (t.Type != JTokenType.Object) { t.WriteTo(writer); return; } JObject o = (JObject)t; writer.WriteStartObject(); WriteJson(writer, o); writer.WriteEndObject(); } private void WriteJson(JsonWriter writer, JObject value) { foreach (var p in value.Properties()) { if (p.Value is JObject) WriteJson(writer, (JObject)p.Value); else p.WriteTo(writer); } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override bool CanConvert(Type objectType) { return true; // works for any type } } public struct PushValue : IDisposable { Func getValue; Action setValue; T oldValue; public PushValue(T value, Func getValue, Action setValue) { if (getValue == null || setValue == null) throw new ArgumentNullException(); this.getValue = getValue; this.setValue = setValue; this.oldValue = getValue(); setValue(value); } #region IDisposable Members // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class. public void Dispose() { if (setValue != null) setValue(oldValue); } #endregion } 

Tenga en cuenta que este convertidor solo escribe; la lectura no está implementada.

Por cierto, su convertidor como está escrito crea JSON con nombres duplicados:

 { "id": 1, "name": null, "name": "value" } 

Esto, aunque no es estrictamente ilegal, generalmente se considera una mala práctica

Si está seguro de que su convertidor no se compartirá entre subprocesos, puede usar una variable miembro en su lugar:

 public class FJson : JsonConverter { bool CannotWrite { get; set; } public override bool CanWrite { get { return !CannotWrite; } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JToken t; using (new PushValue(true, () => CannotWrite, (canWrite) => CannotWrite = canWrite)) { t = JToken.FromObject(value, serializer); } if (t.Type != JTokenType.Object) { t.WriteTo(writer); return; } JObject o = (JObject)t; writer.WriteStartObject(); WriteJson(writer, o); writer.WriteEndObject(); } private void WriteJson(JsonWriter writer, JObject value) { foreach (var p in value.Properties()) { if (p.Value is JObject) WriteJson(writer, (JObject)p.Value); else p.WriteTo(writer); } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override bool CanConvert(Type objectType) { return true; // works for any type } } 

En este esquema, es necesario llamar a JToken.FromObject(Object, JsonSerializer) y pasar el serializador entrante, para que se use la misma instancia de su convertidor FJson . Una vez hecho esto, puede restaurar el [JsonConverter(typeof(FJson))] a su clase A :

 [JsonConverter(typeof(FJson))] public class A { } 

No me gustó la solución publicada anteriormente, así que averigué cómo el serializador realmente serializó el objeto e intentó destilarlo al mínimo:

 public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer ) { JsonObjectContract contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract( value.GetType() ); writer.WriteStartObject(); foreach ( var property in contract.Properties ) { writer.WritePropertyName( property.PropertyName ); writer.WriteValue( property.ValueProvider.GetValue(value)); } writer.WriteEndObject(); } 

Sin problema de desbordamiento de stack y sin necesidad de un indicador de deshabilitación recursiva.

Después de leer (y probar) la solución de Paul Kiar & p.kaneman, diría que parece una tarea desafiante implementar WriteJson . A pesar de que funciona en la mayoría de los casos, hay algunos casos límite que aún no están cubiertos. Ejemplos:

  • métodos public bool ShouldSerialize*()
  • valores null
  • tipos de valor ( struct )
  • atributos del convertidor json
  • ..

Aquí hay (solo) otro bash:

 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { if (ReferenceEquals(value, null)) { writer.WriteNull(); return; } var contract = (JsonObjectContract)serializer .ContractResolver .ResolveContract(value.GetType()); writer.WriteStartObject(); foreach (var property in contract.Properties) { if (property.Ignored) continue; if (!ShouldSerialize(property, value)) continue; var property_name = property.PropertyName; var property_value = property.ValueProvider.GetValue(value); writer.WritePropertyName(property_name); if (property.Converter != null && property.Converter.CanWrite) { property.Converter.WriteJson(writer, property_value, serializer); } else { serializer.Serialize(writer, property_value); } } writer.WriteEndObject(); } private static bool ShouldSerialize(JsonProperty property, object instance) { return property.ShouldSerialize == null || property.ShouldSerialize(instance); } 

No puedo comentar todavía, lo siento mucho … pero solo quería agregar algo a la solución provista por Paul Kiar. Su solución realmente me ayudó.

El código de Paul es corto y simplemente funciona sin ningún tipo de construcción personalizada de objetos. La única adición que me gustaría hacer es insertar un cheque si se ignora la propiedad. Si está configurado para ser ignorado, omita la escritura para esa propiedad:

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JsonObjectContract contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType()); writer.WriteStartObject(); foreach (var property in contract.Properties) { if (property.Ignored) continue; writer.WritePropertyName(property.PropertyName); writer.WriteValue(property.ValueProvider.GetValue(value)); } writer.WriteEndObject(); } 

Al colocar el atributo en la clase A, se lo llama recursivamente. La primera línea en la anulación de WriteJson vuelve a llamar al serializador en la clase A.

 JToken t = JToken.FromObject(value); 

Esto causa una llamada recursiva y, por lo tanto, la StackOverflowException.

De su código, creo que está tratando de aplanar la jerarquía. Probablemente pueda lograr esto colocando el atributo conversor en la propiedad B, lo que evitará la recursión.

 //remove the converter from here public class A { public A() { this.b = new B(); } public int id { get; set; } public string name { get; set; } [JsonConverter(typeof(FJson))] public B b { get; set; } } 

Advertencia: El Json que obtenga aquí tendrá dos llaves llamadas ” nombre ” una de la clase A y la otra de la clase B.