Search code examples
c#jsonserializationjson.netconverters

Custom JsonConverter WriteJson Does Not Alter Serialization of Sub-properties


I always had the impression that the JSON serializer actually traverses your entire object's tree, and executes the custom JsonConverter's WriteJson function on each interface-typed object that it comes across - not so.

I have the following classes and interfaces:

public interface IAnimal
{
    string Name { get; set; }
    string Speak();
    List<IAnimal> Children { get; set; }
}

public class Cat : IAnimal
{
    public string Name { get; set; }
    public List<IAnimal> Children { get; set; }        

    public Cat()
    {
        Children = new List<IAnimal>();
    }

    public Cat(string name="") : this()
    {
        Name = name;
    }

    public string Speak()
    {
        return "Meow";
    }       
}

 public class Dog : IAnimal
 {
    public string Name { get; set; }
    public List<IAnimal> Children { get; set; }

    public Dog()
    {
        Children = new List<IAnimal>();   
    }

    public Dog(string name="") : this()
    {
        Name = name;
    }

    public string Speak()
    {
        return "Arf";
    }

}

To avoid the $type property in the JSON, I've written a custom JsonConverter class, whose WriteJson is

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JToken t = JToken.FromObject(value);

    if (t.Type != JTokenType.Object)
    {
        t.WriteTo(writer);                
    }
    else
    {
        IAnimal animal = value as IAnimal;
        JObject o = (JObject)t;

        if (animal != null)
        {
            if (animal is Dog)
            {
                o.AddFirst(new JProperty("type", "Dog"));
                //o.Find
            }
            else if (animal is Cat)
            {
                o.AddFirst(new JProperty("type", "Cat"));
            }

            foreach(IAnimal childAnimal in animal.Children)
            {
                // ???
            }

            o.WriteTo(writer);
        }
    }
}

In this example, yes, a dog can have cats for children and vice-versa. In the converter, I want to insert the "type" property so that it saves that to the serialization. I have the following setup. (Zoo has only a name and a list of IAnimals. I didn't include it here for brevity and laziness ;))

Zoo hardcodedZoo = new Zoo()
            {   Name = "My Zoo",               
                Animals = new List<IAnimal> { new Dog("Ruff"), new Cat("Cleo"),
                    new Dog("Rover"){
                        Children = new List<IAnimal>{ new Dog("Fido"), new Dog("Fluffy")}
                    } }
            };

            JsonSerializerSettings settings = new JsonSerializerSettings(){
                ContractResolver = new CamelCasePropertyNamesContractResolver() ,                    
                Formatting = Formatting.Indented
            };
            settings.Converters.Add(new AnimalsConverter());            

            string serializedHardCodedZoo = JsonConvert.SerializeObject(hardcodedZoo, settings);

serializedHardCodedZoo has the following output after serialization:

{
  "name": "My Zoo",
  "animals": [
    {
      "type": "Dog",
      "Name": "Ruff",
      "Children": []
    },
    {
      "type": "Cat",
      "Name": "Cleo",
      "Children": []
    },
    {
      "type": "Dog",
      "Name": "Rover",
      "Children": [
        {
          "Name": "Fido",
          "Children": []
        },
        {
          "Name": "Fluffy",
          "Children": []
        }
      ]
    }
  ]
}

The type property shows up on Ruff, Cleo, and Rover, but not for Fido and Fluffy. I guess the WriteJson isn't called recursively. How do I get that type property there?

As an aside, why does it not camel-case IAnimals like I expect it to?


Solution

  • The reason that your converter is not getting applied to your child objects is because JToken.FromObject() uses a new instance of the serializer internally, which does not know about your converter. There is an overload that allows you to pass in the serializer, but if you do so here you will have another problem: since you are inside a converter and you are using JToken.FromObject() to try to serialize the parent object, you will get into an infinite recursive loop. (JToken.FromObject() calls the serializer, which calls your converter, which calls JToken.FromObject(), etc.)

    To get around this problem, you must handle the parent object manually. You can do this without much trouble using a bit of reflection to enumerate the parent properties:

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JObject jo = new JObject();
        Type type = value.GetType();
        jo.Add("type", type.Name);
    
        foreach (PropertyInfo prop in type.GetProperties())
        {
            if (prop.CanRead)
            {
                object propVal = prop.GetValue(value, null);
                if (propVal != null)
                {
                    jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
                }
            }
        }
        jo.WriteTo(writer);
    }
    

    Fiddle: https://dotnetfiddle.net/sVWsE4