I basically have a graph-like structure. Something like this:
public class Whole {
public List<Node> allPossibleNodes = new List<Node>();
}
public class Node {
public List<Node> neighbors = new List<Node>();
}
Now I want to serialize this using following code:
public class WriteTest {
static JsonSerializerOptions options = new () {
WriteIndented = true,
IncludeFields = true,
IgnoreReadOnlyProperties = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
ReferenceHandler = ReferenceHandler.Preserve,
};
public static void write() {
var w = new Whole();
Node? p = null; //previous
for (int i = 0; i < 3; i++) {
var n = new Node();
w.allPossibleNodes.Add(n);
if (p != null) {
n.neighbors.Add(p);
p.neighbors.Add(n);
}
p = n;
}
var json = JsonSerializer.Serialize(w, options);
File.WriteAllText("test.json", json);
}
public static void read() {
var json = File.ReadAllText("test2.json");
var w = JsonSerializer.Deserialize<Whole>(json, options);
}
}
It produces following output:
{
"$id": "1",
"allPossibleNodes": {
"$id": "2",
"$values": [
{
"$id": "3",
"neighbors": {
"$id": "4",
"$values": [
{
"$id": "5",
"neighbors": {
"$id": "6",
"$values": [
{
"$ref": "3"
},
{
"$id": "7",
"neighbors": {
"$id": "8",
"$values": [
{
"$ref": "5"
}
]
}
}
]
}
}
]
}
},
{
"$ref": "5"
},
{
"$ref": "7"
}
]
}
}
Which is problematic in that:
"JsonException: 'A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 64."
or if I set options.MaxDepth = Int.MaxValue;
it could potentially overflow the stack.
{
"$id": "1",
"allPossibleNodes": {
"$id": "2",
"$values": [
{
"$id": "3",
"neighbors": {
"$id": "6",
"$values": [
{
"$ref": "4"
}
]
}
},
{
"$id": "4",
"neighbors": {
"$id": "7",
"$values": [
{
"$ref": "3"
},
{
"$ref": "5"
},
]
}
},
{
"$id": "5",
"neighbors": {
"$id": "8",
"$values": [
{
"$ref": "4"
}
]
}
}
]
}
}
But unfortunately the serializer can't even read that, crying with "'Reference '4' was not found..."
Is there an option to make this work?
There is no way to do exactly what you want. System.Text.Json is a single pass serializer so it cannot look forward to resolve a reference.
As a workaround, you could disable direct serialization of Node.neighbors
and replaces Whole
with a DTO that serializes the allPossibleNodes
list and a table of neighbors as two separate, sequential properties. Doing so would limit the recursion depth to something manageable.
To do this, modify Whole
and Node
as follows, introducing a custom converter for Whole
:
[JsonConverter(typeof(WholeConverter))]
public class Whole {
public List<Node> allPossibleNodes = new List<Node>();
}
public class Node {
[JsonIgnore]
public List<Node> neighbors = new List<Node>();
}
class WholeConverter : JsonConverter<Whole>
{
struct WholeDto
{
[JsonPropertyOrder(1)]
public List<Node>? allPossibleNodes { get; set; }
[JsonPropertyOrder(2)]
public IEnumerable<NeighborItem>? neighborTable { get; set; }
}
record struct NeighborItem(Node node, List<Node> neighbors);
public override Whole? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var dto = JsonSerializer.Deserialize<WholeDto>(ref reader, options);
foreach (var item in dto.neighborTable ?? Enumerable.Empty<NeighborItem>().Where(i => i.node != null))
item.node.neighbors = item.neighbors;
var whole = new Whole();
if (dto.allPossibleNodes != null)
whole.allPossibleNodes = dto.allPossibleNodes;
return whole;
}
public override void Write(Utf8JsonWriter writer, Whole value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer,
new WholeDto
{
allPossibleNodes = value.allPossibleNodes,
neighborTable = value.allPossibleNodes.Where(n => n.neighbors.Count > 0).Select(n => new NeighborItem(n, n.neighbors)),
},
options);
}
The resulting JSON will look like:
{
"allPossibleNodes": {
"$id": "1",
"$values": [
{
"$id": "2"
},
{
"$id": "3"
},
{
"$id": "4"
}
]
},
"neighborTable": {
"$id": "5",
"$values": [
{
"node": {
"$ref": "2"
},
"neighbors": {
"$id": "6",
"$values": [
{
"$ref": "3"
}
]
}
},
{
"node": {
"$ref": "3"
},
"neighbors": {
"$id": "7",
"$values": [
{
"$ref": "2"
},
{
"$ref": "4"
}
]
}
},
{
"node": {
"$ref": "4"
},
"neighbors": {
"$id": "8",
"$values": [
{
"$ref": "3"
}
]
}
}
]
}
}
It's not quite as clean as your desired JSON, but it's still manageable.
Notes:
The code assumes that a Node
will only ever be serialized from with a Whole.allPossibleNodes
list, and that this list does in fact contain all reachable nodes.
If these assumptions are not correct, marking neighbors
with [JsonIgnore]
may cause problems.
If file size is a problem, be sure to set WriteIndented = false
instead of true
.
Setting options.MaxDepth = Int.MaxValue
is no longer required. Nodes will not be serialized recursively, so serialization depth will be capped.
Demo fiddle here.