System.Text.Json.JsonException:è stato rilevato un possibile ciclo di oggetti che non è supportato

System.Text.Json.JsonException:è stato rilevato un possibile ciclo di oggetti che non è supportato

Quando usi System.Text.Json.JsonSerializer per serializzare un oggetto che ha un ciclo, otterrai la seguente eccezione:

Questo è lo stesso problema di cui ho scritto in questo articolo sull'eccezione del ciclo di oggetti di Newtonsoft, tranne che in questo caso utilizza System.Text.Json.JsonSerializer invece di Newtonsoft. Le possibili soluzioni sono simili a quelle mostrate in quell'articolo, ma non esattamente le stesse.

Innanzitutto, cos'è un ciclo di oggetti? I serializzatori funzionano scorrendo ricorsivamente le proprietà di un oggetto. Quando incontra un riferimento a un oggetto che ha già incontrato, significa che c'è un ciclo. Il serializzatore deve gestire questo ciclo, altrimenti ricorrerebbe all'infinito e alla fine otterrebbe un'eccezione StackOverflowException. La strategia predefinita di JsonSerializer per gestire i cicli consiste nel generare un'eccezione.

Ecco un esempio di un oggetto con un riferimento circolare. La classe Child si riferisce alla classe Parent, che fa riferimento alla classe 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)

A causa del riferimento circolare, la chiamata a JsonSerializer.Serialize() genererà la JsonException "ciclo dell'oggetto rilevato".

In questo articolo mostrerò cinque diverse opzioni per risolvere questo problema. Scegli l'opzione che ha più senso nel tuo scenario specifico.

Aggiornato 2022-08-18 per spiegare la nuova opzione in .NET 6.

Opzione 1:utilizzare l'attributo JsonIgnore per fare in modo che il serializzatore ignori la proprietà con il riferimento circolare

Inserisci l'attributo JsonIgnore nelle proprietà con i riferimenti circolari. Questo dice al serializzatore di non tentare di serializzare queste proprietà.

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)

Il JSON risultante è simile al seguente:

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

Se scegli di non serializzare queste informazioni, l'altra parte potrebbe avere problemi con la deserializzazione, perché le proprietà di mamma/papà sono nulle.

Opzione 2 – Rimuovere il riferimento circolare

Potresti aver creato accidentalmente questo riferimento circolare, o forse la proprietà non è importante per te. In entrambi i casi, la soluzione è semplice:rimuovere la proprietà.

Le proprietà di eccezione sono una causa comune di questo problema. In questo esempio, ho una classe Message con una proprietà Exception.

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

Successivamente, genererò un'eccezione, la collocherò su un oggetto e proverò a serializzarlo:

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)

Ciò comporta l'eccezione di riferimento circolare.

Posso risolvere rimuovendo la proprietà Exception. Invece, aggiungerò una proprietà string per contenere il messaggio di eccezione.

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

Opzione 3:usa invece Newtonsoft e usa ReferenceLoopHandling.Ignore (prima di .NET 6)

In .NET 6, hanno aggiunto un'opzione a System.Text.Json.JsonSerializer per ignorare i riferimenti circolari (vedere l'opzione 6 di seguito). Se stai utilizzando una versione precedente a .NET 6, puoi utilizzare Newtonsoft per farlo.

Innanzitutto, aggiungi il pacchetto nuget Newtonsoft.Json. Questo sta usando la Package Manager Console:

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

Quindi usa JsonConvert.SerializeObject() e passa l'opzione ReferenceLoopHandling.Ignore:

using Newtonsoft.Json;

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

Il JSON risultante è simile al seguente:

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

Opzione 4:crea un JsonConverter per personalizzare la modalità di serializzazione dell'oggetto problematico

Supponiamo che tu voglia risolvere questo problema di riferimento circolare senza dover modificare le classi che stai serializzando. Potrebbero anche essere classi di terze parti che non puoi modificare. In ogni caso, puoi personalizzare la serializzazione di qualsiasi oggetto sottoclasse JsonConverter e controllando la serializzazione per quell'oggetto.

Innanzitutto, aggiungi una sottoclasse JsonConverter, come questa:

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)

Quindi usa questo convertitore passandolo nell'elenco JsonSerializerOptions.Converters in questo modo:

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

Questo restituisce il seguente JSON:

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

Opzione 5:utilizzare l'opzione ReferenceHandler.Preserve (in .NET 5)

A partire da .NET 5, hanno aggiunto la proprietà ReferenceHandler a JsonSerializerOption.

Puoi usarlo in questo modo:

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

Quando si serializza, vengono aggiunte le proprietà dei metadati al JSON. Quindi si presenta così:

{
  "$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)

Questo JSON ha proprietà di metadati. Finché il deserializzatore sa come gestire le proprietà dei metadati, non è un problema.

Newtonsoft gestisce le proprietà dei metadati per impostazione predefinita, mentre con System.Text.Json devi specificare la proprietà ReferenceHandler durante la deserializzazione:

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

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

Se non specifichi ReferenceHandler.Preserve qui, otterrai la seguente eccezione:

Se utilizzerai questa opzione per gestire i riferimenti circolari, assicurati che il deserializzatore sappia come gestire le proprietà dei metadati in modo appropriato.

Opzione 6:utilizzare l'opzione ReferenceHandler.IgnoreCycles (in .NET 6)

In .NET 6, hanno aggiunto l'opzione ReferenceHandler.IgnoreCycles a System.Text.Json. Ciò ti consente di ignorare i riferimenti circolari.

Ecco come usarlo:

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

Quando si serializza con questa opzione, annulla i riferimenti circolari. Ecco cosa produce:

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

Se non vuoi che i valori null vengano visualizzati in questo modo, puoi ignorare tutte le proprietà null con l'impostazione DefaultIgnoreCondition:

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