添加对旧JSON结构的向后兼容性支持

时间:2018-11-20 11:19:35

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

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

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

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

1 个答案:

答案 0 :(得分: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属性,那就更好了。这样,我们就可以执行诸如按目录顺序(最后一个,第一个)对名称进行排序的操作,并打印非正式的问候语,例如“嗨,乔!”。

幸运的是,到目前为止,我们已经可靠地输入了数据,名字在姓氏之前,中间有空格,因此我们有一条可行的升级路径:我们可以在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(v3json);

看起来不错!结果如下:

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

通过再次反序列化并转储一些数据,我们可以确认转换器也可以使用新的Version 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