首页 > 解决方案 > 为较旧的 JSON 结构添加向后兼容性支持

问题描述

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

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

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

标签: c#.netjsonxamarinjson.net

解决方案


您如何支持向后兼容性取决于您的“之前”和“之后”模型的不同之处。

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

如果您要用不同的属性替换过时的属性,您可以使用使属性反序列化但不使用 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属性会更好。这样我们就可以做一些事情,比如按目录顺序(最后,第一个)对名称进行排序,并打印非正式的问候语,比如“嗨,乔!”。

幸运的是,我们知道到目前为止已经可靠地输入了数据,名字在姓之前,名字之间有一个空格,所以我们有一个可行的升级路径:我们可以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 读入 a 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


推荐阅读