Saturday, September 26, 2015

How to only Serialize Interface Properties with Json.NET

When you serialize an object with Newtonsoft's Json.NET it will resolve the serialization contract for the type being serialized. This means that if you want to serialize an object so that it matches one of the interfaces that it implements you will need to use a customized contract resolver.

When I first tried to do this I made a completely custom JsonConverter for the type that looked up the properties via reflection and just wrote their values out manually. Unfortunately had the side effect of bypassing all of the features the Newtonsoft provides with regard to decorating classes and customizing the serialization process for that object.

There was a good topic on Stack Overflow about this that led me to the custom contract resolver solution. However the sample implementation there is hard coded to only try to serialize one hard coded type for all serialization.

Below is an implementation (with tests) that allows you to specify a list of interfaces that you want to serialize by, and then if the object being serialized does implement that interface it will fall back on it's default contract.

InterfaceContractResolver Implementation

public class InterfaceContractResolver : DefaultContractResolver
{
    private readonly Type[] _interfaceTypes;
 
    private readonly ConcurrentDictionary<Type, Type> _typeToSerializeMap;
 
    public InterfaceContractResolver(params Type[] interfaceTypes)
    {
        _interfaceTypes = interfaceTypes;
 
        _typeToSerializeMap = new ConcurrentDictionary<Type, Type>();
    }
 
    protected override IList<JsonProperty> CreateProperties(
        Type type,
        MemberSerialization memberSerialization)
    {
        var typeToSerialize = _typeToSerializeMap.GetOrAdd(
            type,
            t => _interfaceTypes.FirstOrDefault(
                it => it.IsAssignableFrom(t)) ?? t);
 
        return base.CreateProperties(typeToSerialize, memberSerialization);
    }
}

Unit Tests

public interface ISample
{
    int Id { get; }
    IList<string> Names { get; }
}
 
public class Sample : ISample
{
    int ISample.Id
    {
        get { return Identity; }
    }
 
    public int Identity { get; set; }
    public IList<string> Names { get; set; }
    public bool IsValid { get; set; }
}
 
public class Example
{
    public int Id { get; set; }
    public string Names { get; set; }
    public bool IsTrue { get; set; }
}
 
public class InterfaceContractResolverTests
{
    [Fact]
    public void SerializeSample()
    {
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new InterfaceContractResolver(typeof (ISample))
        };
 
        var sample = new Sample
        {
            Identity = 42,
            IsValid = true,
            Names = new[] {"Linq", "Sql"}
        };
 
        var sampleJson1 = JsonConvert.SerializeObject(sample);
        var sampleJson2 = JsonConvert.SerializeObject(sample, settings);
 
        Assert.Equal(
            @"{""Identity"":42,""Names"":[""Linq"",""Sql""],""IsValid"":true}",
            sampleJson1);
 
        Assert.Equal(
            @"{""Id"":42,""Names"":[""Linq"",""Sql""]}",
            sampleJson2);
    }
 
    [Fact]
    public void SerializeExample()
    {
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new InterfaceContractResolver(typeof (ISample))
        };
 
        var example = new Example
        {
            Id = 2,
            Names = "Hello World",
            IsTrue = false
        };
 
        var exampleJson1 = JsonConvert.SerializeObject(example);
        var exampleJson2 = JsonConvert.SerializeObject(example, settings);
 
        Assert.Equal(
            @"{""Id"":2,""Names"":""Hello World"",""IsTrue"":false}",
            exampleJson1);
 
        Assert.Equal(exampleJson1, exampleJson2);
    }
}

Enjoy,
Tom

2 comments:

  1. This solution doesn't seem to work, at least with the current version of NewtonSoft.Json. I've found out that the properties in the interface are being ignored by default. If you replace the last line of CreateProperties with this, it solves the issue:

    var props = base.CreateProperties(typeToSerialize, memberSerialization);
    foreach(var prop in props)
    {
    prop.Ignored = false;
    }
    return props;

    ReplyDelete
    Replies
    1. Thanks for letting me know. I would like to go back and update this for .NET Core; if I do, then I will make sure to post a link here to the updated version.

      Delete

Real Time Web Analytics