30 Days Of .NET 6 - Day 8 - Control Of Serialization Of Object Cycles
[C#, .NET, 30 Days Of .NET 6]
When dealing with complex object graphs in memory, it is common to have objects referencing themselves.
Take this example.
We have this class:
public class Animal
{
public string Name { get; set; }
public byte Legs { get; set; }
public Animal Parent { get; set; }
}
We can write code like this:
var cat = new Animal() { Name = "Cat", Legs = 4 };
cat.Parent = cat;
Console.WriteLine($"This is a {cat.Name} and it's parent is {cat.Parent.Name}");
Here the cat
object has a property, Parent
that is a reference to itself - a cyclic reference. That Parent's
Parent
is also the same cat
. And so on
This however is not a problem. The code will compile and run perfectly.
This code should print the following:
This is a Cat and it's parent is Cat
The problem comes when you need to serialize this object with JSON.
The JSON serializer will actually refuse to serialize this object.
You will get the following error:
Unhandled exception. System.Text.Json.JsonException: A possible object cycle was detectef the object depth is larger than the maximum allowed depth of 64
The System.Text.Json serializer allows you to handle such scenarios by passing a JsonSerializerOptions object and setting the appropriate properties.
If you wanted to keep a reference to the object (available even in .NET 5) you would do it like this:
var options = new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.Preserve };
var serializedString = JsonSerializer.Serialize(cat, options);
Console.WriteLine(serializedString);
If you look at the resulting JSON it should look like this:
{"$id":"1","Name":"Cat","Legs":4,"Parent":{"$ref":"1"}}
The magic is happening when you set the ReferenceHanlder.Preserve option.
The serializer injects additional metadata so that it can be able to deserialize it correctly and maintain the hierarchy of objects. Note that even with the additional metadata, it is still valid JSON.
The addition in .NET 6 is you can also instruct the serializer to ignore such object cycles.
This is done by setting the ReferenceHanlder.IgnoreCycles property.
options = new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.IgnoreCycles };
serializedString = JsonSerializer.Serialize(cat, options);
Console.WriteLine(serializedString);
This should print the following:
{"Name":"Cat","Legs":4,"Parent":null}
Note that the parent has explicitly been set to null
, and the rest of the properties do not have any metadata unlike before.
You can handle this even in previous versions by explicitly decorating the Parent
object with the JsonIgnore attribute, like so:
public string Name { get; set; }
public byte Legs { get; set; }
[JsonIgnore]
public Animal Parent { get; set; }
This attribute will instruct the serializer to skip the property altogether.
There are two issues with this approach, however:
- You have to change your class definition
- The property will not appear in the JSON at all, which is different from it being null.
In other words, your output will not be this:
{"Name":"Cat","Legs":4,"Parent":null}
It will be this:
{"Name":"Cat","Legs":4}
Thoughts
Having the flexibility to ignore nested object cycles without having to explicitly decorate the property with a JsonIgnore
attribute is a welcome addition.
The code is in my GitHub
TLDR
System.Text.Json
Serializer has additional flexibility.
Setting the reference handler using the JsonSerializationOptions
to ReferenceHandler.IgnoreCycles
allows you to explicitly set circular object references to null
This is Day 8 of the 30 Days Of .NET 6 where every day I will attempt to explain one new / improved thing in the upcoming release of .NET 6.
Happy hacking!