2

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?

  • Seems like this is not implemented out of the box, see [ReferenceHandler.IgnoreCycles doesn't work with Custom Converters #51715](https://github.com/dotnet/runtime/issues/51715). I'll need to think about what workarounds might be available, but in the meantime, are you always calling the serializer directly as shown in your question, or in practice is asp.net core serializing your model for you? – dbc Oct 27 '21 at 16:30
  • @dbc yes, we call this serializer directly. This object is used for cache between the requests. I've already tried to use a custom reference resolver implementation for this case (added example in the question). But I am a little bit afraid of using internal logic based on $id and $ref manually because it could be changed in the future. – Alexander Sheremetyev Oct 28 '21 at 12:05
  • Your *solution that is based on custom reference handler usage* looks like the only way to go. You might go ahead and [answer your own question](https://stackoverflow.com/help/self-answer). There's another example of a custom reference resolver in the [docs](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-preserve-references#persist-reference-metadata-across-multiple-serialization-and-deserialization-calls) if you would prefer to use that one. – dbc Oct 28 '21 at 13:26

1 Answers1

1

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).

  • $id and $ref have been around in various serializers for a decade. I doubt they're changing. – N-ate Jun 19 '23 at 21:54