7

I have a requirement to add a 'type' property to every object I serialise using Json.Net. I understand Json.Net already supports this out of the box, but in my case, the type name needs to exclude the assembly, and the name of the property must be configurable (neither of which are supported).

I currently have this:

public class TypeConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serialiser)
    {
        JObject jObject = JObject.FromObject(value, serializer);
        jObject.AddFirst(new JProperty("myCustomTypePropertyName"), value.GetType().Name);
        jObject.WriteTo(writer);
    }

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

    public override bool CanConvert(Type objectType)
    {
        return objectType.IsClass;
    }
}

This works for the outer type which is being serialised, but unfortunately the converter is not called for the nested objects. If I add the serialiser into the JObject.FromObject call then I get a self referencing loop exception as it tries to reenter the converter for the outer type.

The only way I can get around this is by manually reflecting and iterating over the properties at each level and serialising them using the serializer parameter, but it's super ugly, even before considering performance.

I would appreciate some help on this; I hope I'm missing something obvious.

(Note: I'm running .NET 3.5, so SerializationBinders are out of the question.)

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
trickdev
  • 627
  • 1
  • 7
  • 14

1 Answers1

11

No, you're not missing anything obvious. Trying to do this with a JsonConverter that handles every kind of class is going to be problematic for reasons you've already seen. JsonConverters work best for handling specific types; they're not so good at generalizing. Fortunately, there is a way to do what you want using a custom IContractResolver instead.

A contract resolver allows us to apply certain serialization behaviors at the property level over a wide range of classes. The idea is to have the resolver fake out an extra "type name" property (or whatever you want it to be called) on each class and install a corresponding IValueProvider to provide the value for that property when it comes time to serialize each object. (That way the serializer never knows that the property doesn't really exist.)

The easiest way to create the resolver is to derive it from the DefaultContractResolver that comes with Json.Net. From there we just need to override the CreateProperties() method and inject our fake property into the list returned from the base class.

Here is the code for the resolver and value provider:

class CustomResolver : DefaultContractResolver
{
    private string customTypePropertyName;
    private IValueProvider valueProvider = new SimpleTypeNameProvider();

    public CustomResolver(string customTypePropertyName)
    {
        this.customTypePropertyName = customTypePropertyName;
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);

        if (type.IsClass && type != typeof(string))
        {
            // Add a phantom string property to every class which will resolve 
            // to the simple type name of the class (via the value provider)
            // during serialization.
            props.Insert(0, new JsonProperty
            {
                DeclaringType = type,
                PropertyType = typeof(string),
                PropertyName = customTypePropertyName,
                ValueProvider = valueProvider,
                Readable = true,
                Writable = false
            });
        }

        return props;
    }

    class SimpleTypeNameProvider : IValueProvider
    {
        public object GetValue(object target)
        {
            return target.GetType().Name;
        }

        public void SetValue(object target, object value)
        {
            return;
        }
    }
}

To use the resolver, create an instance and pass it to the serializer via the JsonSerializerSettings object. Here is a short demo:

class Program
{
    static void Main(string[] args)
    {
        Person p = new Person
        {
            Id = 2,
            Name = "Peter",
            Employer = new Company
            {
                Id = 5,
                Name = "Initech"
            }
        };

        JsonSerializerSettings settings = new JsonSerializerSettings
        {
            ContractResolver = new CustomResolver("MyTypeName"),
            Formatting = Formatting.Indented
        };

        string json = JsonConvert.SerializeObject(p, settings);

        Console.WriteLine(json);
    }
}

class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Company Employer { get; set; }
}

class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Output:

{
  "MyTypeName": "Person",
  "Id": 2,
  "Name": "Peter",
  "Employer": {
    "MyTypeName": "Company",
    "Id": 5,
    "Name": "Initech"
  }
}
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • Ideal - had totally missed the contract resolvers. I had jsonconverter pegged as the only way to modify the serialisation of a given type. Many thanks. – trickdev Jun 12 '14 at 10:52
  • 1
    Ugh, wish I saw this a few hours ago. By the way, would you happen to have any idea how to reverse this process? The same issue exists with converters when deserializing (they essentially remove newtonsoft from the process), and contract resolvers aren't used in deserialization. I'm hoping (and still looking for) some point in the api where json.net asks me "here's a JsonObject, gimme the type and property setters needed for it". As of now, all I see is using the generic deserialization, then parsing out the object graph myself, which is meh. –  Jan 12 '16 at 17:31
  • @Will Use a custom SerializationBinder, see https://stackoverflow.com/questions/11099466/using-a-custom-type-discriminator-to-tell-json-net-which-type-of-a-class-hierarc – sydd Nov 14 '17 at 21:54