9

I'm looking for a way to do deserialization from Json to be version dependent using the data within the Json itself. I'm targeting to use ServiceStack.Text.JsonDeserializer, but can switch to another library.

For example, I'd like to define a data in JSON for v1.0 to be:

{
    version: "1.0"
    condition: "A < B"
}

and then, a next version of the data (say 2.0) to be:

{
    version: "2.0"
    condition: ["A < B", "B = C", "B < 1"]
}

At the end, I want to be able to validate version of the data to know how to deserialize the JSON correctly.

UPDATE:

It looks like there is no any kind of implicit support for version-dependent JSON (de)serialization in known products.

The right solution seems to be to split the task by (de)serializing only version part and then use implicit (de)serializing for the correct type(s).

Gratitudes to everyone who shared knowledge and thoughts on the problem.

Leo Y
  • 659
  • 7
  • 22

5 Answers5

7

What you can do is either the following:

  • Create a base class for the data objects you want to deserialize that contains a version field and nothing else.
  • Make the data classes for your different versions be derived classes of this base class.
  • When you deserialize your data object, first, deserialize it as an instance of your base class - so now you have a POCO object that contains the version number. You can use this to decide which of your derived data classes you should use to deserialize your data (in the simplest case, you can do a switch/case and handle each version individually)

An example (using System.Web.Script.Serialization.JavaScriptSerializer):

class BaseClass 
{
    public int version { get; set; }
}

class FirstVersion: BaseClass 
{
    public string condition { get; set; }
}

class SecondVersion: BaseClass
{
    public IEnumerable<string> condition { get; set; }
}

public void Deserialize (string jsonString)
{
    JavaScriptSerializer serializer = new JavaScriptSerializer();
    BaseClass myData = serializer.Deserialize<BaseClass>(jsonString);

    switch (myData.version) 
    {
        case 1:
            FirstVersion firstVersion = serializer.Deserialize<FirstVersion>(jsonString);
            // ...
            break;
        case 2:
            SecondVersion secondVersion = serializer.Deserialize<SecondVersion>(jsonString);
            // ...
            break;
    }
}

As you can see, this code deserializes the data twice - that may be a problem for you if you are working with large data structures. If you want to avoid that at all costs, you either have to give up static typing or modify the data model of your application.

And here is how it looks like with dynamic:

public void Deserialize (string jsonString)
{
    JavaScriptSerializer serializer = new JavaScriptSerializer();
    dynamic myData = serializer.Deserialize<object>(jsonString);

    if (myData ["version"] == 1) {
        ...
    }
}

There is also the option to write your own custom JavaScriptConverter. That is a lot more work, but I'm pretty sure you can achieve what you want and it will look nicer.

Another advice to consider is never to remove properties from your JSON structure. If you need to modify a property, keep the old one and add a new one instead - this way, old code can always read data from newer code. Of course, this can get out of hand pretty quickly if you modify your data structures a lot...

In Java, you could use Google's GSON library, as it has a built-in support for versioning. I haven't looked into it, but it is open source and if it's really important to you, I guess you can port the implementation to a different language.

  • Thank you. Can you elaborate how I can control derived class creation from within a code of the basic class, as part of the deserialization. Since the instance of the class is already created I cannot re-created and substitute it. It looks inconvenient to have a wrapper that handles versioning and adding to it members of different types or having it create on the fly an internal member that stores the actual data. I'll have a look into JavaScriptConverter. – Leo Y Mar 26 '16 at 11:11
  • It IS inconvenient, but unfortunately there is no universal solution for this problem (that I know of). I updated my answer with a few guidelines that might still help you – Sándor Mátyás Márton Mar 31 '16 at 10:15
1

I suggest that you use json.net is it allows you to add your custom type converts which can be used for versioning.

The problem is not serialization as it will always use the current schema. The problem is when the client uses a different type version that the server that receives the object.

What you need to do is to check the version programatically in your type converter and the convert the value by yourself (in this case convert the string to an array).

Documentation: http://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm

jgauffin
  • 99,844
  • 45
  • 235
  • 372
  • Your statement isn't entirely correct. It relates to a case when a client is sending for modern version than a server supports. Otherwise, a server could be able to support previous formats. I'd prefer to use C# standard means for serialization, such as `ISerializable` interface. – Leo Y Mar 26 '16 at 08:32
0

You might want to use the NewtonSoft.Json NuGET package.

This is kind of a standard within the .NET community. It is also often referred to as Json.NET

You can use it like this (example from official website):

Product product = new Product();

product.Name = "Apple";
product.ExpiryDate = new DateTime(2008, 12, 28);
product.Price = 3.99M;
product.Sizes = new string[] { "Small", "Medium", "Large" };

string output = JsonConvert.SerializeObject(product);
//{
//  "Name": "Apple",
//  "ExpiryDate": "2008-12-28T00:00:00",
//  "Price": 3.99,
//  "Sizes": [
//    "Small",
//    "Medium",
//    "Large"
//  ]
//}

Product deserializedProduct = JsonConvert.DeserializeObject<Product>(output);
Nick N.
  • 12,902
  • 7
  • 57
  • 75
  • Given an example that I added to the question, how can I change the code to handle different versions? – Leo Y Mar 25 '16 at 11:00
  • @LeoY Since you've rephrased your question, my answer has gone out of line. I think the way of serializing does not matter, it is how you implement your versioning that matters in this case. – Nick N. Mar 25 '16 at 13:39
  • I was looking for some kind of implicit support. It seems there is none in this case and probably NewtonSoft's serializer is most convenient to do the custom implementation, since `ServiceStack.Text.JsonSerializer` is too much DTO oriented. – Leo Y Mar 26 '16 at 11:12
0

If you are willing to switch to JSON.net, then there is a simpler way of doing it. You don't have to use a BaseClass containing version and you don't have to parse twice. The trick is to use JObject and then query JSON for the version:

JObject obj = JObject.Parse(json);
string version = obj.SelectToken("$.Version")?.ToString();

Then you can proceed as Sándor did with the bonus part that you can use JObject to get your dto instead of re-reading json:

ConditionsDto v1Dto = obj.ToObject<ConditionsDto>(readSerializer);

Putting it all together:

public static ConditionsBusinessObject Parse(string json)
{
    JObject obj = JObject.Parse(json);
    string version = obj.SelectToken("$.Version")?.ToString();

    JsonSerializer readSerializer = JsonSerializer.CreateDefault(/*You might want to place your settings here*/);
    switch (version)
    {
        case null: //let's assume that there are some old files out there with no version at all
        //and that these are equivalent to the version 1
        case "1":
            ConditionsDto v1Dto = obj.ToObject<ConditionsDto>(readSerializer);
            if (v1Dto == null) return null; //or throw
            List<string> convertedConditions = new List<string> {v1Dto.Condition}; //See what I've done here?
            return new ConditionsBusinessObject(convertedConditions);
        case "2":
            ConditionsDtoV2 v2Dto = obj.ToObject<ConditionsDtoV2>(readSerializer);
            return v2Dto == null ? null //or throw
                : new ConditionsBusinessObject(v2Dto.Condition);
        default:
            throw new Exception($"Unsupported version {version}");
    }
}

For reference here are the classes that I have:

public class ConditionsDto
{
    public string Version { get; set; }
    public string Condition { get; set; }
}

public class ConditionsDtoV2
{
    public string Version { get; set; }
    public List<string> Condition { get; set; }
}

public class ConditionsBusinessObject
{

    public ConditionsBusinessObject(List<string> conditions)
    {
        Conditions = conditions;
    }

    public List<string> Conditions { get; }
}

and a couple of tests to wrap it up:

    [Test]
    public void TestV1()
    {
        string v1 = @"{
    Version: ""1"",
    Condition: ""A < B""
}";

        //JsonHandler is where I placed Parse()
        ConditionsBusinessObject fromV1 = JsonHandler.Parse(v1);
        Assert.AreEqual(1, fromV1.Conditions.Count);
        Assert.AreEqual("A < B", fromV1.Conditions[0]);
    }
    [Test]
    public void TestV2()
    {
        string v2 = @"{
    Version: ""2"",
    Condition: [""A < B"", ""B = C"", ""B < 1""]
}";

        ConditionsBusinessObject fromV2 = JsonHandler.Parse(v2);
        Assert.AreEqual(3, fromV2.Conditions.Count);
        Assert.AreEqual("A < B", fromV2.Conditions[0]);
        Assert.AreEqual("B = C", fromV2.Conditions[1]);
        Assert.AreEqual("B < 1", fromV2.Conditions[2]);
    }

In a normal real world application, the //See what I've done here? part is where you will have to do all your conversion chores. I didn't do anything smart there, I just wrapped the single condition to a list to make it compatible with the current business object. As you could guess though, this can explode as the application evolves. This answer in softwareengineering SE has more details in the theory behind versioned JSON data so you might want to have a look in order to know what to expect.

One final word about performance impact of reading to JObject and then converting to the dto is that I haven't done any measurements but I expect it to be better than parsing twice. If I find out that this is not true, I will update the answer accordingly.

Stelios Adamantidis
  • 1,866
  • 21
  • 36
-1

Take a look at

System.Web.Script.Serialization.JavaScriptSerializer

Sample

var ser = new JavaScriptSerializer();
var result = (IReadOnlyDictionary<string, object>)ser.DeserializeObject(json);

if(result["version"] == "1.0")
{
    // You expect a string for result["condition"]
}
else
{
    // You expect an IEnumerable<string> for result["condition"]
}
synapse
  • 130
  • 4
  • Given an example that I added to the question, how this class would help? – Leo Y Mar 25 '16 at 10:59
  • the question was about handling versioning (i.e. different schemas for the same type) – jgauffin Mar 25 '16 at 12:31
  • You can manage different versions by using the DeserializeObject(string) method. I will post an example. – synapse Mar 25 '16 at 12:42
  • You suggest that I parse all by myself while I'm looking to place this information into the class implementation exactly to avoid dealing with per-field parsing – Leo Y Mar 26 '16 at 11:14