Version dependent Json deserialization

Viewed 4487

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.

5 Answers

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.

Related