System.Text.Json.JsonException:se detectó un posible ciclo de objetos que no es compatible

System.Text.Json.JsonException:se detectó un posible ciclo de objetos que no es compatible

Cuando usa System.Text.Json.JsonSerializer para serializar un objeto que tiene un ciclo, obtendrá la siguiente excepción:

Este es el mismo problema sobre el que escribí en este artículo sobre la excepción del ciclo de objetos de Newtonsoft, excepto que en este caso usa System.Text.Json.JsonSerializer en lugar de Newtonsoft. Las posibles soluciones son similares a las que se muestran en ese artículo, pero no exactamente iguales.

Primero, ¿qué es un ciclo de objetos? Los serializadores funcionan recorriendo recursivamente las propiedades de un objeto. Cuando encuentra una referencia a un objeto que ya encontró, significa que hay un ciclo. El serializador tiene que lidiar con este ciclo, de lo contrario, se repetiría infinitamente y eventualmente obtendría una StackOverflowException. La estrategia predeterminada de JsonSerializer para tratar con ciclos es lanzar una excepción.

Aquí hay un ejemplo de un objeto con una referencia circular. La clase Child se refiere a la clase Parent, que se refiere a la clase Child:

Parent harry = new Parent()
{
	Name = "Harry"
};
Parent mary = new Parent()
{
	Name = "Mary"
};
harry.Children = new List<Child>()
{
	new Child() { Name = "Barry", Dad=harry, Mom=mary }
};
mary.Children = harry.Children;

var json = JsonSerializer.Serialize(harry, new JsonSerializerOptions() 
{
	WriteIndented = true
});

Console.WriteLine(json);
Code language: C# (cs)

Debido a la referencia circular, la llamada a JsonSerializer.Serialize() generará la excepción JsonException "ciclo de objeto detectado".

En este artículo mostraré cinco opciones diferentes para resolver este problema. Elija la opción que tenga más sentido en su escenario específico.

Actualizado el 18 de agosto de 2022 para explicar la nueva opción en .NET 6.

Opción 1:use el atributo JsonIgnore para hacer que el serializador ignore la propiedad con la referencia circular

Coloque el atributo JsonIgnore en las propiedades con las referencias circulares. Esto le dice al serializador que no intente serializar estas propiedades.

public class Child
{
	[System.Text.Json.Serialization.JsonIgnore]
	public Parent Mom { get; set; }
	[System.Text.Json.Serialization.JsonIgnore]
	public Parent Dad { get; set; }
	public string Name { get; set; }
}
Code language: C# (cs)

El JSON resultante se ve así:

{
	"Children": [{
		"Name": "Barry"
	}],
	"Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

Si elige no serializar esta información, el otro lado puede tener problemas para deserializar, porque las propiedades de mamá/papá son nulas.

Opción 2:eliminar la referencia circular

Es posible que haya creado accidentalmente esta referencia circular, o quizás la propiedad no sea importante para usted. En cualquier caso, la solución es simple:elimine la propiedad.

Las propiedades de excepción son una causa común de este problema. En este ejemplo, tengo una clase de mensaje con una propiedad de excepción.

public class Message
{
	public string Name { get; set; }
	public Exception Exception { get; set; }
	public void Throw()
	{
		throw new Exception();
	}
}
Code language: C# (cs)

A continuación, lanzaré una excepción, la pegaré en un objeto e intentaré serializarlo:

try
{
	var msg = new Message()
	{
		Name = "hello world"
	};
	msg.Throw();
}
catch (Exception ex)
{
	var errorMessage = new Message()
	{
		Name = "Error",
		Exception = ex
	};

	var json = JsonSerializer.Serialize(errorMessage, new JsonSerializerOptions()
	{
		WriteIndented = true
	});

	Console.WriteLine(json);
}
Code language: C# (cs)

Esto da como resultado la excepción de referencia circular.

Puedo resolver eliminando la propiedad Exception. En su lugar, agregaré una propiedad de cadena para contener el mensaje de excepción.

public class Message
{
	public string Name { get; set; }
	public string ExceptionMessage { get; set; }
	public void Throw()
	{
		throw new Exception();
	}
}
Code language: C# (cs)

Opción 3:use Newtonsoft en su lugar y use ReferenceLoopHandling.Ignore (antes de .NET 6)

En .NET 6, agregaron una opción a System.Text.Json.JsonSerializer para ignorar las referencias circulares (consulte la opción 6 a continuación). Si está usando una versión anterior a .NET 6, puede usar Newtonsoft para hacer esto.

Primero, agregue el paquete nuget Newtonsoft.Json. Esto es usando la Consola del administrador de paquetes:

 Install-Package Newtonsoft.Json
Code language: PowerShell (powershell)

Luego use JsonConvert.SerializeObject() y pase la opción ReferenceLoopHandling.Ignore:

using Newtonsoft.Json;

var json = JsonConvert.SerializeObject(harry, Formatting.Indented,
                    new JsonSerializerSettings()
                    {
                        ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                    });
Code language: C# (cs)

El JSON resultante se ve así:

{
  "Children": [
    {
      "Mom": {
        "Name": "Mary"
      },
      "Name": "Barry"
    }
  ],
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

Opción 4:crea un JsonConverter para personalizar cómo se serializa el objeto problemático

Supongamos que desea resolver este problema de referencia circular sin tener que cambiar las clases que está serializando. Incluso pueden ser clases de terceros que no puede cambiar. En cualquier caso, puede personalizar la serialización de cualquier objeto subclasificando JsonConverter y controlando la serialización de ese objeto.

Primero, agregue una subclase JsonConverter, como esta:

public class ChildJsonConverter : JsonConverter<Child>
{
	public override bool CanConvert(Type objectType)
	{
		return objectType == typeof(Child);
	}

	public override Child Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		return null; //Doesn't handle deserializing
	}

	public override void Write(Utf8JsonWriter writer, Child value, JsonSerializerOptions options)
	{
		writer.WriteStartObject();
		writer.WriteString(nameof(value.Name), value.Name);
		writer.WriteString(nameof(value.Mom), value.Mom?.Name);
		writer.WriteString(nameof(value.Dad), value.Dad?.Name);
		writer.WriteEndObject();
	}
}
Code language: C# (cs)

Luego use este convertidor pasándolo a la lista JsonSerializerOptions.Converters de esta manera:

var options = new JsonSerializerOptions()
{
	WriteIndented = true
};
options.Converters.Add(new ChildJsonConverter());
var json = JsonSerializer.Serialize(harry, options);
Code language: C# (cs)

Esto genera el siguiente JSON:

{
  "Children": [
    {
      "Name": "Barry",
      "Mom": "Mary",
      "Dad": "Harry"
    }
  ],
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

Opción 5:utilice la opción ReferenceHandler.Preserve (en .NET 5)

A partir de .NET 5, agregaron la propiedad ReferenceHandler a JsonSerializerOption.

Puedes usarlo así:

var json = JsonSerializer.Serialize(harry, new JsonSerializerOptions()
{
	WriteIndented = true,
	ReferenceHandler = ReferenceHandler.Preserve
});
Code language: C# (cs)

Cuando serializa, agrega propiedades de metadatos al JSON. Entonces se ve así:

{
  "$id": "1",
  "Children": {
    "$id": "2",
    "$values": [
      {
        "$id": "3",
        "Mom": {
          "$id": "4",
          "Children": {
            "$ref": "2"
          },
          "Name": "Mary"
        },
        "Dad": {
          "$ref": "1"
        },
        "Name": "Barry"
      }
    ]
  },
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

Este JSON tiene propiedades de metadatos. Siempre que el deserializador sepa cómo manejar las propiedades de los metadatos, no hay problema.

Newtonsoft maneja las propiedades de los metadatos de manera predeterminada, mientras que con System.Text.Json debe especificar la propiedad ReferenceHandler cuando está deserializando:

var parent = Newtonsoft.Json.JsonConvert.DeserializeObject<Parent>(json);

var parent2 = JsonSerializer.Deserialize<Parent>(json, new JsonSerializerOptions()
{
	ReferenceHandler = ReferenceHandler.Preserve
});
Code language: C# (cs)

Si no especifica ReferenceHandler.Preserve aquí, obtendrá la siguiente excepción:

Si va a utilizar esta opción para manejar referencias circulares, asegúrese de que el deserializador sepa cómo manejar las propiedades de los metadatos de manera adecuada.

Opción 6:utilice la opción ReferenceHandler.IgnoreCycles (en .NET 6)

En .NET 6, agregaron la opción ReferenceHandler.IgnoreCycles a System.Text.Json. Esto le permite ignorar las referencias circulares.

Así es como se usa:

var json = JsonSerializer.Serialize(harry, new JsonSerializerOptions()
{
	WriteIndented = true,
	ReferenceHandler = ReferenceHandler.IgnoreCycles
});
Code language: C# (cs)

Cuando serializa con esta opción, anula las referencias circulares. Esto es lo que esto genera:

{
  "Children": [
    {
      "Mom": {
        "Children": null,
        "Name": "Mary"
      },
      "Dad": null,
      "Name": "Barry"
    }
  ],
  "Name": "Harry"
}
Code language: JSON / JSON with Comments (json)

Si no desea que los valores nulos se muestren así, puede ignorar todas las propiedades nulas con la configuración DefaultIgnoreCondition:

new JsonSerializerOptions()
{
	WriteIndented = true,
	ReferenceHandler = ReferenceHandler.IgnoreCycles,
	DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
}
Code language: C# (cs)