【问题标题】:Adding backward compatibility support for an older JSON structure为旧的 JSON 结构添加向后兼容性支持
【发布时间】:2019-04-22 19:56:26
【问题描述】:

我为 android 开发了一个应用程序,它将序列化的域模型以 JSON 文件的形式存储到本地存储中。现在的问题是,有时我会更改域模型(新功能)并希望能够轻松从本地存储中加载 JSON 文件的先前结构。我该怎么做?

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

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

【问题讨论】:

    标签: c# .net json xamarin json.net


    【解决方案1】:

    您如何支持向后兼容性取决于您的“之前”和“之后”模型的差异程度。

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

    如果您要用不同的属性替换过时的属性,您可以使用Making a property deserialize but not serialize with json.net 中描述的技术将旧属性迁移到新属性。

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


    让我们看一些例子。假设我们正在编写一个应用程序来保存一些关于人的信息。我们将从最简单的模型开始:Person 类,该类具有人名的单个属性。

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

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

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

    这为我们提供了以下 JSON。

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

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


    好的,现在说我们要增强应用程序以跟踪人们的生日。这对于向后兼容性来说不是问题,因为我们只是要添加一个新属性;它不会以任何方式影响现有数据。下面是 Person 类与新属性的样子:

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

    为了测试它,我们可以将版本 1 的数据反序列化到这个新模型中,然后将一个新人添加到列表中并将模型序列化回 JSON。 (我还将添加一个格式化选项以使 JSON 更易于阅读。)

    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 现在的样子:

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

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


    好的,现在假设我们已经意识到仅使用单个 Name 属性还不够健壮。如果我们有单独的 FirstNameLastName 属性会更好。这样我们就可以按照目录顺序(最后,第一个)对姓名进行排序,并打印非正式的问候语,例如“Hi, Joe!”。

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

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

    • 使其set 方法设置FirstNameLastName 属性,如上所述;
    • 删除其get 方法,以便Name 属性不会写入JSON;
    • 将其设为私有,使其不再是 Person 的公共接口的一部分;
    • 添加 [JsonProperty] 属性,以便 Json.Net 仍然可以“看到”它,即使它是私有的。

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

    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:

    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);
    

    看起来不错!结果如下:

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

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


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

    首先让我们创建我们需要的新类。我们当然需要Address 类:

    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 属性以将每个人链接到一个地址。

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

    最后,我们需要一个根级别的新类来保存人员和地址列表。让我们也给它一个Version 属性,以防我们将来需要对数据模型进行更改:

    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 将是一个包含两个数组的对象。

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

    这是转换器的代码:

    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 方法,如下所示:

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

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

    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"
        }
      ]
    }
    

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

    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

    【讨论】:

    • 我有一个具有三个原始属性的模型,其中一个更改为数据结构属性。
    • 添加了一堆示例。希望这会有所帮助。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多