27

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?

Evan Frisch
  • 1,334
  • 5
  • 22
  • 40
Mickael Caruso
  • 8,721
  • 11
  • 40
  • 72

4 Answers4

41

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

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
2

Here's an idea, instead of doing the reflection on every property, iterate through the normally serialized JObject and then changed the token of properties you're interested in.

That way you can still leverage all the ''JsonIgnore'' attributes and other attractive features built-in.

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

    if (jToken.Type == JTokenType.Object)
    {
        JObject jObject = (JObject)jToken;
        ...
        AddRemoveSerializedProperties(jObject, val);
        ...
    }
    ...
}

And then

private void AddRemoveSerializedProperties(JObject jObject, MahMan baseContract)
   {
       jObject.AddFirst(....);

        foreach (KeyValuePair<string, JToken> propertyJToken in jObject)
        {
            if (propertyJToken.Value.Type != JTokenType.Object)
                continue;

            JToken nestedJObject = propertyJToken.Value;
            PropertyInfo clrProperty = baseContract.GetType().GetProperty(propertyJToken.Key);
            MahMan nestedObjectValue = clrProperty.GetValue(baseContract) as MahMan;
            if(nestedObj != null)
                AddRemoveSerializedProperties((JObject)nestedJObject, nestedObjectValue);
        }
    }
womp
  • 115,835
  • 26
  • 236
  • 269
GettnDer
  • 392
  • 2
  • 14
  • where is jObject coming from? – whitneyland Oct 31 '17 at 20:00
  • Hi Lee, sorry about that, the jToken is castable to a jObject if the jToken type is 'Object' I added the missing line, if you have any other questions poke me – GettnDer Oct 31 '17 at 20:06
  • What is `MahMan` in this context? – Joseph Oct 27 '22 at 19:25
  • @Joseph whatever custom type you're dealing with. Some BaseObject of your own model. In this case I always needed to serialize an additional property to all objects of type "MahMan", but could be anything you need to work with . (to be fair this was a while back) – GettnDer Oct 29 '22 at 01:56
2

I had this issue using two custom converters for a parent and child type. A simpler method I found is that since an overload of JToken.FromObject() takes a serializer as a parameter, you can pass along the serializer you were given in WriteJson(). However you need to remove your converter from the serializer to avoid a recursive call to it (but add it back in after):

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    serializer.Converters.Remove(this);
    JToken jToken = JToken.FromObject(value, serializer);
    serializer.Converters.Add(this);

    // Perform any necessary conversions on the object returned
}
Framnk
  • 59
  • 1
  • 6
  • 2
    This is a bad practice: 1. It is not threat-safe 2. It will fail to serialize child nodes of the same type the same way as this parent (for missing the current serializer) 3. If the FormObject fails, the state of your seralizer has changed 4. If it not fails, the order of applying converters might have changed. – Corniel Nobel Apr 14 '21 at 06:32
0

Here is a hacky solution to your problem that gets the work done and looks tidy.

public class MyJsonConverter : JsonConverter
{
    public const string TypePropertyName = "type";
    private bool _dormant = false;

    /// <summary>
    /// A hack is involved:
    ///     " JToken.FromObject(value, serializer); " creates amn infinite loop in normal circumstances
    ///     for that reason before calling it "_dormant = true;" is called.
    ///     the result is that this JsonConverter will reply false to exactly one "CanConvert()" call.
    ///     this gap will allow to generate a a basic version without any extra properties, and then add them on the call with " JToken.FromObject(value, serializer); ".
    /// </summary>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        _dormant = true;
        JToken t = JToken.FromObject(value, serializer);
        if (t.Type == JTokenType.Object && value is IContent)
        {
            JObject o = (JObject)t;
            o.AddFirst(new JProperty(TypePropertyName, value.GetType().Name));
            o.WriteTo(writer);
        }
        else
        {
            t.WriteTo(writer);
        }
    }

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

    public override bool CanRead => false;

    public override bool CanConvert(Type objectType)
    {
        if (_dormant)
        {
            _dormant = false;
            return false;
        }
        return true;
    }
}
Daniel B
  • 11
  • 3