Resolve cycle references of complex type during JSON serialization using System.Text.Json.Serialization.JsonConverter

Viewed 446

There is a complex type with a reference to the object of the same type (sometimes to the same object):

public class User
{
    public string Name { get; set; }

    public int Age { get; set; }

    public User Reference { get; set; }
}

There is a custom JsonConverter (System.Text.Json.Serialization) implementation to deserialize this object avoiding some special properties.

public class UserJsonConverter : JsonConverter<User>
{
    public override User Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, User value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        // write the name property only and ignore the age
        writer.WriteString(nameof(value.Name), value.Name);

        writer.WritePropertyName(nameof(value.Reference));
        JsonSerializer.Serialize(writer, value.Reference, options);

        writer.WriteEndObject();
    }
}

But it is not clear how to configure the reference resolving in a case when the object points to itself. Example:

var user = new User
{
    Age = 10,
    Name = "username"
};

user.Reference = user;

var options = new JsonSerializerOptions();
options.ReferenceHandler = ReferenceHandler.Preserve;
options.Converters.Add(new UserJsonConverter());

var result = JsonSerializer.Serialize(user, user.GetType(), options);

The exception happens:

System.Text.Json.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. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles

We use System.Text.Json, Version=5.0.0.0

All '$ref' and '$id' based methods of a default object converter are internal and not available for use. The only way I see is simplifying a User object in some DTO before serialization and using it without this custom converter at all.

But maybe someone knows is there a correct way to resolve these references in custom JsonConverter?

1 Answers

A solution that is based on custom reference handler usage

UserJsonConverter changes:

public override void Write(Utf8JsonWriter writer, User value, JsonSerializerOptions options)
{
    // use reference handler manually
    var v_resolver = options.ReferenceHandler?.CreateResolver();
    if (v_resolver != null)
    {
        var v_refID = v_resolver.GetReference(value, out bool alreadyExists);
        if (alreadyExists)
        {
            writer.WriteStartObject();
            writer.WriteString(JsonEncodedText.Encode("$ref", encoder: null), v_refID);
            writer.WriteEndObject();
            return;
        }
        else
        {
            writer.WriteStartObject();
            writer.WriteString(JsonEncodedText.Encode("$id", encoder: null), v_refID);
        }
    }

    //writer.WriteStartObject();

    ...
}

Usage changes:

...
var options = new JsonSerializerOptions();
options.ReferenceHandler = new CutomReferenceHandler();
...

Custom reference handler implementation is available here: https://github.com/dotnet/docs/issues/21777#issuecomment-736751404 or docs

It works but I am a little bit afraid to use it this way because the internal reference resolver logic could be changed in the future ($id, $refs).

Related