Oleh Nechytailo
Oleh Nechytailo

Reputation: 2195

Custom reference loop handling

I'm trying to implement custom reference loop handling. All I need is to write empty object in place of nested object.

Expected result

 { Id:1, Field:"Value", NestedObject:{Id:1}}

I created JsonConverter

public class SerializationConverter : JsonConverter
{
    public override bool CanRead { get { return false; } }
    public override bool CanWrite { get { return true; } }


    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Form) || typeof(Form).IsAssignableFrom(objectType);
    }

    private HashSet<Form> serializedForms = new HashSet<Form>();

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value == null)
            writer.WriteNull();

        var f = (Form)value;
        if (!serializedForms.Add(f))
            writer.WriteRawValue("{Id:" + f.Id.Value + "}");
        else
            serializer.Serialize(writer, value);
    }
}

But, as expected, serializer on internal call of serializer.Serialize(writer, value) invokes my converter again.

I'm trying to replace serialization result only if object was already serialized, otherwise use default serialization behavior.

Upvotes: 2

Views: 1307

Answers (1)

Brian Rogers
Brian Rogers

Reputation: 129667

First, I want to mention that Json.Net has a built-in PreserveReferencesHandling setting that can handle this sort of thing for you automatically, without need for a special converter. With PreserveReferencesHandling set to All, Json.Net assigns internal reference IDs to each object and writes special $id and $ref properties into the JSON to track the references. For your example, the JSON output would look like this:

{"$id":"1","Id":1,"Field":"Value","NestedObject":{"$ref":"1"}}

You'll notice this is very similar to the desired output from your question. This also has the advantage that it can easily be deserialized back into the original object graph with all the references preserved, again without having to implement anything special.

But let's assume for the moment that you have your own reasons for wanting to implement custom reference loop handling, and take a look at why your code doesn't work.

When Json.Net encounters a JsonConverter for an object, it assumes that the converter is going to handle writing whatever JSON is needed for that object. So, if you want certain properties included, you have to write them out yourself. You can use the serializer to help write parts of the object, but you can't just hand the whole object to the serializer and say "serialize this", because it's just going to end up calling back into your converter.

In most cases, doing this will result in an infinite loop. In your case, it doesn't, because you added the form to the HashSet in the first call to WriteJson. When the serializer calls back the second time, the other branch is taken because the form is already in the set. So, the whole JSON for the object ends up being {Id:1} instead of what you really wanted.

One way to prevent the serializer from calling back into your converter is to create a new instance of the JsonSerializer inside the converter and use that one instead of the one passed into the WriteJson method. The new instance will not have a reference to your converter so your Form would be serialized normally.

Unfortunately, this idea won't work either: if you don't have a reference to the converter on the inner serializer, then there's no way for Json.Net to know how to do your special serialization handling for the NestedObject! Instead, it will simply get omitted, because we would be forced to set ReferenceLoopHandling to Ignore to avoid errors. So you see, you have a catch-22.

So how can we get this to work? Well, let's take a step back and redefine what you really want to happen in terms of output:

  1. If we encounter a form we're already seen, we just want to output the Id.
  2. Otherwise, add the form the list of forms we've seen, then output the Id, Field and NestedObject.

Note that in both cases we want to output the Id, so we can simplify the logic to this:

  1. Always output the Id
  2. If we encounter a Form we haven't already seen, add the form the list of forms we've seen, then output the Field and NestedObject.

To make things easy, we can use a JObject to collect the properties we want to output, and then simply write it to the writer at the end.

Here is the revised code:

public class SerializationConverter : JsonConverter
{
    public override bool CanRead { get { return false; } }
    public override bool CanWrite { get { return true; } }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Form) || typeof(Form).IsAssignableFrom(objectType);
    }

    private HashSet<Form> serializedForms = new HashSet<Form>();

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Form f = (Form)value;

        JObject jo = new JObject();
        jo.Add("Id", f.Id);

        if (serializedForms.Add(f))
        {
            jo.Add("Field", f.Field);
            if (f.NestedObject != null)
            {
                jo.Add("NestedObject", JToken.FromObject(f.NestedObject, serializer));
            }
        }

        jo.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Now let's test it:

class Program
{
    static void Main(string[] args)
    {
        Form form = new Form
        {
            Id = 1,
            Field = "Value",
        };
        form.NestedObject = form;

        JsonSerializerSettings settings = new JsonSerializerSettings
        {
            Converters = new List<JsonConverter> { new SerializationConverter() },
            ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
        };

        string json = JsonConvert.SerializeObject(form, settings);
        Console.WriteLine(json);
    }
} 

class Form
{
    public int Id { get; set; }
    public string Field { get; set; }
    public Form NestedObject { get; set; }
}

And here is the output:

{"Id":1,"Field":"Value","NestedObject":{"Id":1}}

Looks good so far. How about something more rigorous:

class Program
{
    static void Main(string[] args)
    {
        List<Form> forms = new List<Form>
        {
            new Form 
            { 
                Id = 1, 
                Field = "One", 
                NestedObject = new Form
                {
                    Id = 2,
                    Field = "Two"
                }
            },
            new Form
            {
                Id = 3,
                Field = "Three"
            },
            new Form
            {
                Id = 4,
                Field = "Four"
            },
            new Form
            {
                Id = 5,
                Field = "Five"
            }
        };

        forms[0].NestedObject.NestedObject = forms[3];
        forms[1].NestedObject = forms[0].NestedObject;
        forms[2].NestedObject = forms[1];

        JsonSerializerSettings settings = new JsonSerializerSettings
        {
            Converters = new List<JsonConverter> { new SerializationConverter() },
            ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
            Formatting = Formatting.Indented
        };

        string json = JsonConvert.SerializeObject(forms, settings);
        Console.WriteLine(json);
    }
}

Output:

[
  {
    "Id": 1,
    "Field": "One",
    "NestedObject": {
      "Id": 2,
      "Field": "Two",
      "NestedObject": {
        "Id": 5,
        "Field": "Five"
      }
    }
  },
  {
    "Id": 3,
    "Field": "Three",
    "NestedObject": {
      "Id": 2
    }
  },
  {
    "Id": 4,
    "Field": "Four",
    "NestedObject": {
      "Id": 3
    }
  },
  {
    "Id": 5
  }
]

EDIT

If your Form class has a large number of fields, you may want to use reflection instead of listing out the properties individually in the converter. Here is what the WriteJson method would look like using reflection:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    Form f = (Form)value;

    JObject jo = new JObject();
    if (serializedForms.Add(f))
    {
        foreach (PropertyInfo prop in value.GetType().GetProperties())
        {
            if (prop.CanRead)
            {
                object propVal = prop.GetValue(value);
                if (propVal != null)
                {
                    jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
                }
            }
        }
    }
    else
    {
        jo.Add("Id", f.Id);
    }

    jo.WriteTo(writer);
}

Upvotes: 2

Related Questions