1. Introduzione
Un riferimento ciclico (o circolare) si verifica quando un oggetto contiene direttamente o indirettamente un riferimento a un altro oggetto che alla fine punta di nuovo al primo.
Esempio reale
Facciamo un esempio pratico per la nostra libreria di libri. Supponiamo di avere la classe Book che ha la proprietà Author, e la classe Author che ha la proprietà Books di tipo List<Book>, così tiene traccia di tutti i suoi libri.
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
public List<Book> Books { get; set; } = new List<Book>();
}
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
}
Ora, se creiamo un autore e un libro, impostiamo i collegamenti, otteniamo un "circolo chiuso":
var author = new Author { Name = "Marsel Prust", BirthYear = 1871 };
var book = new Book { Title = "Verso la casa di Swann", Author = author };
author.Books.Add(book);
// Fatto, ora author punta a book e book punta a author!
Perché è un problema?
Quando serializzi un oggetto così in JSON, il serializer percorre le proprietà. Vede che l'autore ha libri, dentro i quali di nuovo l'autore... che di nuovo contiene libri... che di nuovo contengono autori... e così all'infinito.
author -> books[] -> author -> books[] ...
È come guardare in uno specchio di fronte a un altro specchio — i riflessi si perdono nell'infinito. Solo che invece dei bei riflessi ottieni un overflow dello stack (StackOverflowException).
2. Come reagisce il serializer ai riferimenti ciclici?
Errore di serializzazione
Per impostazione predefinita System.Text.Json non gestisce i riferimenti ciclici. Se provi a serializzare una struttura del genere, riceverai l'eccezione JsonException: "A possible object cycle was detected".
Esempio che causerà l'errore:
string json = JsonSerializer.Serialize(author); // BAM! JsonException
Visualizzazione:
graph TD;
Author --> Book;
Book --> Author;
3. Come risolvere il problema dei riferimenti ciclici?
Vediamo diversi approcci pratici, ognuno con pro e contro. Come avrai capito, non esiste un "flag magico" universale (peccato).
Rimuovere i cicli prima della serializzazione
La cosa più semplice — non creare il ciclo. Prima della serializzazione azzerare (o ignorare) i riferimenti che portano al ciclo.
Come si presenta in pratica:
// Temporaneamente togliamo il riferimento dell'autore ai libri
var authorToSerialize = new Author
{
Name = author.Name,
BirthYear = author.BirthYear,
Books = null // oppure non includere proprio la proprietà
};
string json = JsonSerializer.Serialize(authorToSerialize);
// Ora tutto si è serializzato senza problemi!
Pro: semplice, veloce, chiaro.
Contro: perdi parte dei dati (dopo la deserializzazione non ricostruirai i riferimenti inversi).
Usare l'attributo [JsonIgnore]
Puoi marcare la proprietà che causa il ciclo come ignorata:
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
[JsonIgnore]
public List<Book> Books { get; set; }
}
Ora serializzando l'autore, i suoi libri non saranno salvati. È simile al metodo sopra, ma dichiarativo e senza "pulizia" manuale.
Pro: più semplice, meno rischio di dimenticare di pulire il riferimento.
Contro: l'informazione sui libri dell'autore è persa nel JSON.
Usare identificatori invece di oggetti nidificati
Se è importante mantenere entrambi i lati del collegamento (autori e libri) ma si vuole evitare il ciclo, usa identificatori unici invece di oggetti nidificati:
public class Book
{
public string Title { get; set; }
public int AuthorId { get; set; } // al posto di Author
}
public class Author
{
public int AuthorId { get; set; }
public string Name { get; set; }
// non teniamo i libri o teniamo la lista dei loro Id
}
Nel JSON ora ci sono identificatori invece di oggetti. È un approccio comune in DB, REST API e sistemi con riferimenti univoci.
Pro: niente cicli, JSON compatto, i riferimenti si possono ricostruire tramite Id.
Contro: si rompe il modello oggetto tradizionale, alla deserializzazione serve una ricerca per Id.
Mini-tabella di confronto:
| Approccio | Risolto il problema dei cicli? | Perdita di dati? | Applicabilità |
|---|---|---|---|
| [JsonIgnore] | Sì | Sì | Quando la nidificazione non è critica |
| Rimuovere il riferimento manualmente | Sì | Sì | Veloce prima della serializzazione |
| Usare Id invece di oggetto | Sì | No* | REST, database, sistemi complessi |
* I dati non sono persi, ma non sono immediatamente disponibili (serve una ricerca per Id).
4. Come insegnare a System.Text.Json a serializzare i riferimenti ciclici?
A partire da .NET 5, JsonSerializerOptions ha una modalità per i riferimenti: options.ReferenceHandler = ReferenceHandler.Preserve.
Questa modalità usa campi speciali $id e $ref per gli oggetti ripetuti.
Esempio
var options = new JsonSerializerOptions
{
WriteIndented = true,
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve
};
string json = JsonSerializer.Serialize(author, options);
Console.WriteLine(json);
Il JSON risultante sarà così:
{
"$id": "1",
"Name": "Marsel Prust",
"BirthYear": 1871,
"Books": {
"$id": "2",
"$values": [
{
"$id": "3",
"Title": "Verso la casa di Swann",
"Author": {
"$ref": "1"
}
}
]
}
}
- $id — identificatore unico dell'oggetto nel JSON
- $ref — riferimento a un oggetto già serializzato
Alla deserializzazione tutto si ricostruisce correttamente (senza cicli infiniti e senza errori di stack).
Caratteristiche e limitazioni
- Questo JSON è inconsueto per il frontend: la maggior parte dei client JS non capisce $id/$ref senza logica aggiuntiva.
- La dimensione del JSON è maggiore, è più difficile da debug-are a occhio.
- Funziona solo abilitando esplicitamente ReferenceHandler.Preserve.
- Non riguarda i tipi valore (lì i cicli non possono esistere).
Come deserializzare questo JSON?
Esattamente come un JSON normale, ma usa le stesse JsonSerializerOptions:
var deserializedAuthor = JsonSerializer.Deserialize<Author>(json, options);
5. E con Newtonsoft.Json (Json.NET)?
Storicamente Newtonsoft.Json gestiva i cicli prima di System.Text.Json. Per questo ci sono l'attributo [JsonObject(IsReference = true)] e impostazioni globali di serializzazione.
Attributi per i riferimenti
[JsonObject(IsReference = true)]
public class Author
{
public string Name { get; set; }
public List<Book> Books { get; set; }
}
[JsonObject(IsReference = true)]
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
}
Poi serializzi così:
var settings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
Formatting = Formatting.Indented
};
string json = JsonConvert.SerializeObject(author, settings);
Alla fine ottieni un JSON con $id e $ref, simile alla modalità ReferenceHandler.Preserve.
Breve conclusione
- Se lo scambio è tra applicazioni .NET — abilita la serializzazione dei riferimenti (ReferenceHandler.Preserve o PreserveReferencesHandling).
- Se i dati vanno a JavaScript/altre client — rompi i cicli: [JsonIgnore], pulizia dei riferimenti o passaggio a Id.
6. Come evitare errori e mal di testa
Molto spesso i principianti (e anche i più esperti) si scontrano con il crash del serializer a causa dei cicli. Ricorda: se collezioni/proprietà puntano l'una all'altra — rivedi l'architettura del modello.
Non esitare a usare [JsonIgnore] per le proprietà che non servono allo scambio esterno.
Una trappola classica è serializzare relazioni "molti a molti" (per esempio studenti ↔ corsi). Senza rompere i cicli o abilitare la serializzazione di riferimenti, non funziona.
Nelle REST API di solito si invia l'oggetto "in una direzione": per esempio, il libro conosce l'autore, e l'autore ha solo gli Id dei libri (o non conosce affatto i libri in questo contratto).
GO TO FULL VERSION