Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding a custom type name to all classes during serialisation with Json.NET

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

like image 730
trickdev Avatar asked Jun 11 '14 20:06

trickdev


1 Answers

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"
  }
}
like image 121
Brian Rogers Avatar answered Sep 19 '22 12:09

Brian Rogers