Saturday, December 24, 2016

Deserializing base type property with JSON.Net and having it work in WCF as well

Flexible JSON input Sometimes we want to serialize and deserialize objects that are dynamic in nature, like having different types of content based on a property. However, the strong typed nature of C# and the limitations of serializers make this frustrating.


In this post I will be discussing several things:
  • How to deserialize a JSON property that can have a value of multiple types that are not declared
  • How to serialize and deserialize a property declared as a base type in WCF
  • How to serialize it back using JSON.Net

The code can all be downloaded from GitHub. The first phase of the project can be found here.

Well, the scenario was this: I had a string in the database that was in JSON format. I would deserialize it using JSON.Net and use the resulting object. The object had a property that could be one of several types, but no special notation to describe its concrete type (like the $type notation for JSON.Net or the __type notation for DataContractSerializer). The only way I knew what type it was supposed to be was a type integer value.

My solution was to declare the property as JToken, meaning anything goes there, then deserialize it manually when I have both the JToken value and the integer type value. However, passing the object through a WCF service, which uses DataContractSerializer and which would throw the exception System.Runtime.Serialization.InvalidDataContractException: Type 'Newtonsoft.Json.Linq.JToken' is a recursive collection data contract which is not supported. Consider modifying the definition of collection 'Newtonsoft.Json.Linq.JToken' to remove references to itself..

I thought I could use two properties that pointed at the same data: one for JSON.Net, which would use JToken, and one for WCF, which would use a base type. It should have been easy, but it was not. People have tried this on the Internet and declared it impossible. Well, it's not!

Let's work with some real data. Database JSON:
{
  "MyStuff": [
    {
      "Configuration": {
        "Wheels": 2
      },
      "ConfigurationType": 1
    },
    {
      "Configuration": {
        "Legs": 4
      },
      "ConfigurationType": 2
    }
  ]
}

Here is the code that works:
[DataContract]
[JsonObject(MemberSerialization.OptOut)]
public class Stuff
{
    [DataMember]
    public List<Thing> MyStuff;
}

[DataContract]
[KnownType(typeof(Vehicle))]
[KnownType(typeof(Animal))]
public class Configuration
{
}

[DataContract]
public class Vehicle : Configuration
{
    [DataMember]
    public int Wheels;
}

[DataContract]
public class Animal : Configuration
{
    [DataMember]
    public int Legs;
}

[DataContract]
public class Thing
{
    private Configuration _configuration;
    private JToken _jtoken;

    [DataMember(Name = "ConfigurationType")]
    public int ConfigurationType;

    [IgnoreDataMember]
    public JToken Configuration
    {
        get
        {
            return _jtoken;
        }
        set
        {
            _jtoken = value;
            _configuration = null;
        }
    }

    [JsonIgnore]
    [DataMember(Name = "Configuration")]
    public Configuration ConfigurationObject
    {
        get
        {
            if (_configuration == null)
            {
                switch (ConfigurationType)
                {
                    case 1: _configuration = Configuration.ToObject<Vehicle>(); break;
                    case 2: _configuration = Configuration.ToObject<Animal>(); break;
                }
            }
            return _configuration;
        }
        set
        {
            _configuration = value;
            _jtoken = JRaw.FromObject(value);
        }
    }
}

public class ThingContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member,
            Newtonsoft.Json.MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);
        if (member.Name == "Configuration") prop.Ignored = false;
        return prop;
    }
}

So now this is what happens:
  • DataContractSerializer ignores the JToken property, and doesn't throw an exception
  • Instead it takes ConfigurationObject and serializes/deserializes it as "Configuration", thanks to the KnownTypeAttributes decorating the Configuration class and the DataMemberAttribute that sets the property's serialization name (in WCF only)
  • JSON.Net ignores DataContract as much as possible (JsonObject(MemberSerialization.OptOut)) and both properties, but then un-ignores the Configuration property when we use the ThingContractResolver
  • DataContractSerializer will add a property called __type to the resulting JSON, so that it knows how to deserialize it.
  • In order for this to work, the JSON deserialization of the original text needs to be done with JSON.Net (no __type property) and the JSON coming to the WCF service to be correctly decorated with __type when it comes to the server to be saved in the database

If you need to remove the __type property from the JSON, try the solution here: JSON.NET how to remove nodes. I didn't actually need to do it.

Of course, it would have been simpler to just replace the default serializer of the WCF service to Json.Net, but I didn't want to introduce other problems to an already complex system.


A More General Solution


To summarize, it would have been grand if I could have used IgnoreDataMemberAttribute and JSON.Net would just ignore it. To do that, I could use another ContractResolver:
public class JsonWCFContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member,
        Newtonsoft.Json.MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);
        var hasIgnoreDataMember = member.IsDefined(typeof(IgnoreDataMemberAttribute), false);
        var hasJsonIgnore = member.IsDefined(typeof(JsonIgnoreAttribute), false);
        if (hasIgnoreDataMember && !hasJsonIgnore)
        {
            prop.Ignored = false;
        }
        return prop;
    }
}
This class now automatically un-ignores properties declared with IgnoreDataMember that are also not declared with JsonIgnore. You might want to create your own custom attribute so that you don't modify JSON.Net's default behavior.

Now, while I thought it would be easy to implement a JsonConverter that works just like the default one, only it uses this contract resolver by default, it was actually a pain in the ass, mainly because there is no default JsonConverter. Instead, I declared the default contract resolver in a static constructor like this:
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
    ContractResolver=new JsonWCFContractResolver()
};

Now the JSON.Net operations don't need an explicitly declared contract resolver. Updated source code on GitHub


Links that helped

Configure JSON.NET to ignore DataContract/DataMember attributes
replace WCF built-in JavascriptSerializer with Newtonsoft Json.Net json serializer
Cannot return JToken from WCF Web Service
Serialize and deserialize part of JSON object as string with WCF

0 comments: