To clarify the issue: the error is occurring because in C# land your instances have references to each other in memory:
var session = new SessionRec
{
Id = 1,
Title = "Session",
Speakers = new List<SpeakerRec>()
};
var speaker = new SpeakerRec
{
Id = 1,
First = "Speaker",
Last = "Speaks",
Sessions = new List<SessionRec>()
};
session.Speakers.Add(speaker);
speaker.Sessions.Add(session);
// session.Speakers[0] == speaker
// speaker.Sessions[0] == session
But during serialization when representing these object instances as text (JSON), the serializer will essentially try to do the following:
{
"Id": 1,
"First": "Speaker",
"Last": "Speaks",
"Sessions": [
{
"Id": 1,
"Title": "Session"
"Speakers": [
{
"Id": 1,
"First": "Speaker",
"Last": "Speaks",
"Sessions": [
{
"Id": 1,
"Title": "Session",
// You see where this is going...
}
]
}
]
}
]
}
And so you get JsonException: A possible object cycle was detected
. As you found, there is one way to handle this in the System.Text.Json SerializerOptions in .NET 5: ReferenceHandler.Preserve. But as you also found, this approach adds metadata properties in order to properly represent the object cycle as JSON.
So on to your question:
how I can get a clean JSON output with speakers and session details
The short answer is that there is currently no support to to simply ignore object cycles with System.Text.Json (in .NET 5). See this open GitHub issue to follow the request for it, which is currently slated for .NET 6.
With that said, your options to handle this scenario are (in order of what I would personally recommend):
1 - Create a separate object layer for the API
This one's pretty simple, and you don't need to worry about serializing objects with potential cycles or decorating your data objects with serialization attributes. For this, you'd create corresponding DTOs for your data objects, map them, and simply return them:
public class SessionRecDto
{
public int Id { get; set; }
public string Title { get; set; }
}
public class SpeakerRecDto
{
public int Id { get; set; }
public string First { get; set; }
public string Last { get; set; }
public List<SessionRecDto> Sessions { get; set; }
}
This entirely removes the object cycle in the API and represents exactly how (I assume) you expect it to look:
{
"Id": 1,
"First": "Speaker",
"Last": "Speaks",
"Sessions": [
{
"Id": 1,
"Title": "Session"
}
]
}
2 - Use Newtonsoft.Json
Newtonsoft.Json does have out of the box support for ignoring object cycles (ReferenceLoopHandling):
string json = JsonConvert.SerializeObject(speaker, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
{
"Id": 1,
"First": "Speaker",
"Last": "Speaks",
"Sessions": [
{
"Id": 1,
"Title": "Session",
"Speakers": []
}
]
}
3 - Ignore serializing the property by decorating the data class
public class SessionRec
{
public int Id { get; set; }
public string Title { get; set; }
[JsonIgnore]
public virtual List<SpeakerRec> Speakers { get; set; }
}
This will effectively look the same as option 1.