我为Android开发了一个应用程序,该程序将JSON文件中的序列化域模型存储到本地存储中。现在的事情是,有时我会更改域模型(新功能),并希望可以选择轻松从本地存储中轻松加载JSON文件的先前结构。我该怎么办?
我曾想过匿名反序列化对象并使用自动映射器,但是我想在走这条路之前先听听别人的想法。
如果需要域模型的代码示例(之前和之后),我将提供。谢谢大家。
答案 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
属性是不够鲁棒的。如果我们有单独的FirstName
和LastName
属性,那就更好了。这样,我们就可以执行诸如按目录顺序(最后一个,第一个)对名称进行排序的操作,并打印非正式的问候语,例如“嗨,乔!”。
幸运的是,到目前为止,我们已经可靠地输入了数据,名字在姓氏之前,中间有空格,因此我们有一条可行的升级路径:我们可以在Name
属性上拆分空间并从中填充两个新属性。完成之后,我们想将Name
属性视为过时的;我们不希望将来再将其写回JSON。
让我们对模型进行一些更改以实现这些目标。在添加了两个新的字符串属性FirstName
和LastName
之后,我们需要如下更改旧的Name
属性:
set
方法设置为FirstName
和LastName
属性; 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