为较旧的JSON结构添加向后兼容性支持 [英] Adding backward compatibility support for an older JSON structure

查看:53
本文介绍了为较旧的JSON结构添加向后兼容性支持的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我为Android开发了一个应用程序,该程序将序列化的域模型以JSON文件存储到本地存储中.现在的事情是,有时我会更改域模型(新功能),并希望可以选择轻松从本地存储中轻松加载JSON文件的先前结构.我该怎么办?

I have developed an app for android which stores a serialized domain model in a JSON file to the local storage. Now the thing is, sometimes I make changes to the domain model (new features) and want to have the option to easily load a previous structure of the JSON file from the local storage. How can I do this?

我曾想过匿名反序列化对象并使用自动映射器,但是我想在走这条路之前先听听别人的想法.

I thought of deserializing the object anonymously and using auto-mapper, but I want to hear others' ideas first before going this path.

如果需要域模型的代码示例(之前和之后),我将提供.谢谢大家.

If a code example of the domain model is needed (before and after), I'll provide. Thanks everyone.

推荐答案

如何支持向后兼容取决于您的之前"和之前"的区别.和之后".模型将成为.

How you support backward compatibility depends on how different your "before" and "after" models are going to be.

如果您只是要添加新属性,那么这根本不会造成问题;您只需将旧的JSON反序列化为新模型,它就可以正常工作而不会出错.

If you are just going to be adding new properties, then this should not pose a problem at all; you can just deserialize the old JSON into the new model and it will work just fine without errors.

如果要用不同的属性替换过时的属性,则可以使用中描述的技术,对属性进行反序列化,但不使用json.net进行序列化.将旧属性迁移到新属性.

If you are replacing obsolete properties with different properties, you can use techniques described in Making a property deserialize but not serialize with json.net to migrate old properties to new.

如果要进行重大的结构更改,则可能需要为每个版本使用不同的类.序列化模型时,请确保将 Version 属性(或其他可靠的标记)写入JSON.然后,当需要反序列化时,可以将JSON加载到 JToken ,检查 Version 属性,然后从 JToken 填充适用于该版本的模型.如果需要,可以将此逻辑封装到 JsonConverter 类.

If you are making big structural changes, then you may want to use different classes for each version. When you serialize the models, ensure that a Version property (or some other reliable marker) is written into the JSON. Then when it is time to deserialize, you can load the JSON into a JToken, inspect the Version property and then populate the appropriate model for the version from the JToken. If you want, you can encapsulate this logic into a JsonConverter class.

让我们看一些例子.假设我们正在编写一个应用程序,其中包含有关人的一些信息.我们将从最简单的模型开始:一个 Person 类,该类具有该人的姓名的单个属性.

Let's walk through some examples. Say we are writing an application which keeps some information about people. We'll start with the simplest possible model: a Person class which has a single property for the person's name.

public class Person  // Version 1
{
    public string Name { get; set; }
}

让我们创建一个数据库".人(这里我只使用一个简单的列表)并将其序列化.

Let's create a "database" of people (I'll just use a simple list here) and serialize it.

List<Person> people = new List<Person>
{
    new Person { Name = "Joe Schmoe" }
};
string json = JsonConvert.SerializeObject(people);
Console.WriteLine(json);

这为我们提供了以下JSON.

That gives us the following JSON.

[{"Name":"Joe Schmoe"}]

提琴: https://dotnetfiddle.net/NTOnu2

好的,现在说我们要增强应用程序以跟踪人们的生日.对于向后兼容,这将不是问题,因为我们将要添加一个新属性.它不会以任何方式影响现有数据.这是具有新属性的 Person 类的外观:

OK, now say we want to enhance the application to keep track of people's birthdays. This will not be a problem for backward compatibility because we're just going to be adding a new property; it won't affect the existing data in any way. Here's what the Person class looks like with the new property:

public class Person  // Version 2
{
    public string Name { get; set; }
    public DateTime? Birthday { get; set; }
}

要测试它,我们可以将版本1数据反序列化到这个新模型中,然后在列表中添加一个新人员并将该模型序列化回JSON.(我还将添加一个格式设置选项,以使JSON易于阅读.)

To test it, we can deserialize the Version 1 data into this new model, then add a new person to the list and serialize the model back to JSON. (I'll also add a formatting option to make the JSON easier to read.)

List<Person> people = JsonConvert.DeserializeObject<List<Person>>(json);
people.Add(new Person { Name = "Jane Doe", Birthday = new DateTime(1988, 10, 6) });
json = JsonConvert.SerializeObject(people, Formatting.Indented);
Console.WriteLine(json);

一切正常.这是JSON现在的样子:

Everything works great. Here's what the JSON looks like now:

[
  {
    "Name": "Joe Schmoe",
    "Birthday": null
  },
  {
    "Name": "Jane Doe",
    "Birthday": "1988-10-06T00:00:00"
  }
]

提琴: https://dotnetfiddle.net/pftGav

好的,现在让我们说我们已经意识到仅使用单个 Name 属性是不够鲁棒的.最好有单独的 First 和 属性.这样,我们就可以执行诸如按目录顺序(最后一个,第一个)对名称进行排序以及打印诸如乔!"之类的非正式问候的事情.

Alright, now let's say we've realized that just using a single Name property isn't robust enough. It would be better if we had separate FirstName and LastName properties instead. That way we can do things like sort the names in directory order (last, first) and print informal greetings like "Hi, Joe!".

幸运的是,我们知道数据到目前为止已经可靠地输入了,名字和姓氏之间是空格,因此我们有一条可行的升级路径:我们可以拆分 Name 属性,并从中填充两个新属性.完成之后,我们想将 Name 属性视为过时的;我们不希望将来再将其写回JSON.

Fortunately, we know that the data has been reliably entered so far with the first name preceding the last name and a space between them, so we have a viable upgrade path: we can split the Name property on the space and fill the two new properties from it. After we do that, we want to treat the Name property as obsolete; we don't want it written back to the JSON in the future.

让我们对模型进行一些更改以实现这些目标.在添加了两个新的字符串属性 First LastName 之后,我们需要如下更改旧的 Name 属性:

Let's make some changes to our model to accomplish these goals. After adding the two new string properties FirstName and LastName, we need to change the old Name property as follows:

  • 使它的 set 方法按照上述说明设置 FirstName LastName 属性;
  • 删除其 get 方法,以使 Name 属性不会写入JSON;
  • 将其设为私有,因此它不再是 Person 的公共接口的一部分;
  • 添加 [JsonProperty] 属性,以便Json.Net仍然可以看到"即使它是私人的.
  • Make its set method set the FirstName and LastName properties as explained above;
  • Remove its get method so that the Name property does not get written to JSON;
  • Make it private so it is no longer part of the public interface of Person;
  • Add a [JsonProperty] attribute so that Json.Net can still "see" it even though it is private.

当然,我们必须更新使用 Name 属性的任何其他代码,以使用新属性.这是我们的 Person 类现在的样子:

And of course, we'll have to update any other code that uses the Name property to use the new properties instead. Here is what our Person class looks like now:

public class Person  // Version 3
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime? Birthday { get; set; }

    // This property is here to support transitioning from Version 2 to Version 3
    [JsonProperty]
    private string Name
    {
        set
        {
            if (value != null)
            {
                string[] parts = value.Trim().Split(' ');
                if (parts.Length > 0) FirstName = parts[0];
                if (parts.Length > 1) LastName = parts[1];
            }
        }
    }
}

为演示一切正常,让我们将第2版JSON加载到此模型中,按姓氏对人员进行排序,然后将其重新序列化为JSON:

To demonstrate that everything works, let's load our Version 2 JSON into this model, sort the people by last name and then reserialize it to JSON:

List<Person> people = JsonConvert.DeserializeObject<List<Person>>(json);
people = people.OrderBy(p => p.LastName).ThenBy(p => p.FirstName).ToList();
json = JsonConvert.SerializeObject(people, Formatting.Indented);
Console.WriteLine(json);

看起来不错!结果如下:

Looks good! Here is the result:

[
  {
    "FirstName": "Jane",
    "LastName": "Doe",
    "Birthday": "1988-10-06T00:00:00"
  },
  {
    "FirstName": "Joe",
    "LastName": "Schmoe",
    "Birthday": null
  }
]    

提琴: https://dotnetfiddle.net/T8NXMM

现在是大个子.假设我们要添加一个新功能来跟踪每个人的家庭住址.但重要的是,人们可以共享相同的地址,在这种情况下,我们不希望重复数据.这需要对我们的数据模型进行重大更改,因为到目前为止,它只是一小部分人.现在,我们需要第二个地址列表,并且需要一种将人员绑定到地址的方法.当然,我们仍然希望支持读取所有旧数据格式.我们该怎么做?

Now for the big one. Let's say we want add a new feature to keep track of each person's home address. But the kicker is, people can share the same address, and we don't want duplicate data in that case. This requires a big change to our data model, because up until now it's just been a list of people. Now we need a second list for the addresses, and we need a way to tie the people to the addresses. And of course we still want to support reading all the old data formats. How can we do this?

首先,让我们创建所需的新类.我们当然需要一个 Address 类:

First let's create the new classes we will need. We need an Address class of course:

public class Address
{
    public int Id { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string PostalCode { get; set; }
    public string Country { get; set; }
}

我们可以重用相同的 Person 类;我们需要做的唯一更改是添加一个 AddressId 属性,以将每个人链接到一个地址.

We can reuse the same Person class; the only change we need is to add an AddressId property to link each person to an address.

public class Person
{
    public int? AddressId { get; set; }
    ...
}

最后,我们需要一个在根级别的新类来保存人员和地址列表.让我们还为它提供一个 Version 属性,以防将来需要更改数据模型:

Lastly, we need a new class at the root level to hold the lists of people and addresses. Let's also give it a Version property in case we need to make changes to the data model in the future:

public class RootModel
{
    public string Version { get { return "4"; } }
    public List<Person> People { get; set; }
    public List<Address> Addresses { get; set; }
}

仅此而已;现在最大的问题是我们如何处理不同的JSON?在版本3和更早的版本中,JSON是对象数组.但是对于这种新模型,JSON将是一个包含两个数组的对象.

That's it for the model; now the big issue is how do we handle the differing JSON? In versions 3 and earlier, the JSON was an array of objects. But with this new model, the JSON will be an object containing two arrays.

解决方案是使用自定义 JsonConverter 用于新模型.我们可以将JSON读取到 JToken 中,然后根据发现的内容(数组与对象)以不同的方式填充新模型.如果有一个对象,我们将检查刚刚添加到模型中的新版本号属性.

The solution is to use a custom JsonConverter for the new model. We can read the JSON into a JToken and then populate the new model differently depending on what we find (array vs. object). If we get an object, we'll check for the new version number property we just added to the model.

这是转换器的代码:

public class RootModelConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(RootModel);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        RootModel model = new RootModel();
        if (token.Type == JTokenType.Array)
        {
            // we have a Version 3 or earlier model, which is just a list of people.
            model.People = token.ToObject<List<Person>>(serializer);
            model.Addresses = new List<Address>();
            return model;
        }
        else if (token.Type == JTokenType.Object)
        {
            // Check that the version is something we are expecting
            string version = (string)token["Version"];
            if (version == "4")
            {
                // all good, so populate the current model
                serializer.Populate(token.CreateReader(), model);
                return model;
            }
            else
            {
                throw new JsonException("Unexpected version: " + version);
            }
        }
        else
        {
            throw new JsonException("Unexpected token: " + token.Type);
        }
    }

    // This signals that we just want to use the default serialization for writing
    public override bool CanWrite
    {
        get { return false; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

要使用转换器,我们创建一个实例并将其传递给 DeserializeObject 方法,如下所示:

To use the converter, we create an instance and pass it to the DeserializeObject method like this:

RootModelConverter converter = new RootModelConverter();
RootModel model = JsonConvert.DeserializeObject<RootModel>(json, converter);

现在我们已经加载了模型,我们可以更新数据以显示Joe和Jane住在同一地址,然后再次将其序列化:

Now that we have the model loaded, we can update the data to show that Joe and Jane live at the same address and serialize it back out again:

model.Addresses.Add(new Address
{
    Id = 1,
    Street = "123 Main Street",
    City = "Birmingham",
    State = "AL",
    PostalCode = "35201",
    Country = "USA"
});

foreach (var person in model.People)
{
    person.AddressId = 1;
}

json = JsonConvert.SerializeObject(model, Formatting.Indented);
Console.WriteLine(json);

这是生成的JSON:

{
  "Version": 4,
  "People": [
    {
      "FirstName": "Jane",
      "LastName": "Doe",
      "Birthday": "1988-10-06T00:00:00",
      "AddressId": 1
    },
    {
      "FirstName": "Joe",
      "LastName": "Schmoe",
      "Birthday": null,
      "AddressId": 1
    }
  ],
  "Addresses": [
    {
      "Id": 1,
      "Street": "123 Main Street",
      "City": "Birmingham",
      "State": "AL",
      "PostalCode": "35201",
      "Country": "USA"
    }
  ]
}

通过再次反序列化并转储一些数据,我们可以确认转换器也可以使用新的Version 4 JSON格式:

We can confirm the converter works with the new Version 4 JSON format as well by deserializing it again and dumping out some of the data:

model = JsonConvert.DeserializeObject<RootModel>(json, converter);
foreach (var person in model.People)
{
    Address addr = model.Addresses.FirstOrDefault(a => a.Id == person.AddressId);
    Console.Write(person.FirstName + " " + person.LastName);
    Console.WriteLine(addr != null ? " lives in " + addr.City + ", " + addr.State : "");
}

输出:

Jane Doe lives in Birmingham, AL
Joe Schmoe lives in Birmingham, AL

提琴: https://dotnetfiddle.net/4lcDvE

这篇关于为较旧的JSON结构添加向后兼容性支持的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆