【问题标题】:Version dependent Json deserialization依赖版本的 Json 反序列化
【发布时间】:2016-03-25 10:28:18
【问题描述】:

我正在寻找一种方法来使用 Json 本身中的数据从 Json 进行反序列化以依赖于版本。 我的目标是使用ServiceStack.Text.JsonDeserializer,但可以切换到另一个库。

例如,我想将 v1.0 的 JSON 数据定义为:

{
    version: "1.0"
    condition: "A < B"
}

然后,数据的下一个版本(比如 2.0)将是:

{
    version: "2.0"
    condition: ["A < B", "B = C", "B < 1"]
}

最后,我希望能够验证数据的版本以了解如何正确反序列化 JSON。

更新:

在已知产品中似乎没有任何对版本相关 JSON(反)序列化的隐式支持。

正确的解决方案似乎是通过(反)序列化仅版本部分来拆分任务,然后对正确的类型使用隐式(反)序列化。

感谢所有就该问题分享知识和想法的人。

【问题讨论】:

  • 请举一个你的意图的小例子
  • 只有一个版本的 JSON。你到底是什么意思?
  • 您的意思是您的 JSON 之上有密钥“版本:1.x”吗?
  • PTwr:是的,这就是我的意思。我添加了示例来详细说明问题。

标签: c# json-deserialization


【解决方案1】:

您可以执行以下任一操作:

  • 为要反序列化的数据对象创建一个基类,其中包含 version 字段,仅此而已。
  • 让不同版本的数据类成为这个基类的派生类。
  • 当您反序列化您的数据对象时,首先,将其反序列化为您的基类的一个实例 - 所以现在您有一个包含版本号的 POCO 对象。您可以使用它来决定您应该使用哪个派生数据类来反序列化您的数据(在最简单的情况下,您可以进行 switch/case 并单独处理每个版本)

一个例子(使用System.Web.Script.Serialization.JavaScriptSerializer):

class BaseClass 
{
    public int version { get; set; }
}

class FirstVersion: BaseClass 
{
    public string condition { get; set; }
}

class SecondVersion: BaseClass
{
    public IEnumerable<string> condition { get; set; }
}

public void Deserialize (string jsonString)
{
    JavaScriptSerializer serializer = new JavaScriptSerializer();
    BaseClass myData = serializer.Deserialize<BaseClass>(jsonString);

    switch (myData.version) 
    {
        case 1:
            FirstVersion firstVersion = serializer.Deserialize<FirstVersion>(jsonString);
            // ...
            break;
        case 2:
            SecondVersion secondVersion = serializer.Deserialize<SecondVersion>(jsonString);
            // ...
            break;
    }
}

如您所见,此代码对数据进行了两次反序列化 - 如果您使用大型数据结构,这可能会给您带来问题。如果您想不惜一切代价避免这种情况,您必须放弃静态类型或修改应用程序的数据模型。

下面是dynamic 的样子:

public void Deserialize (string jsonString)
{
    JavaScriptSerializer serializer = new JavaScriptSerializer();
    dynamic myData = serializer.Deserialize<object>(jsonString);

    if (myData ["version"] == 1) {
        ...
    }
}

还可以选择编写自己的自定义JavaScriptConverter。这需要做更多的工作,但我很确定你可以实现你想要的,而且看起来会更好。

另一个要考虑的建议是永远不要从 JSON 结构中删除属性。如果您需要修改属性,请保留旧属性并添加新属性 - 这样,旧代码始终可以从新代码中读取数据。当然,如果您大量修改数据结构,这可能很快就会失控......

在 Java 中,您可以使用 Google 的 GSON 库,因为它有一个 built-in support for versioning。我没有调查它,但它是open source,如果它对你来说真的很重要,我想你可以将实现移植到不同的语言。

【讨论】:

  • 谢谢。作为反序列化的一部分,您能否详细说明如何从基本类的代码中控制派生类的创建。由于该类的实例已经创建,我无法重新创建并替换它。拥有一个处理版本控制并向其添加不同类型的成员或让它动态创建存储实际数据的内部成员的包装器看起来很不方便。我会看看 JavaScriptConverter。
  • 这很不方便,但不幸的是,这个问题没有通用的解决方案(据我所知)。我更新了我的答案,提供了一些可能仍然对您有帮助的指南
【解决方案2】:

我建议您使用 json.net,它允许您添加可用于版本控制的自定义类型转换。

问题不在于序列化,因为它将始终使用当前架构。问题是当客户端使用与接收对象的服务器不同的类型版本时。

您需要做的是在类型转换器中以编程方式检查版本并自行转换值(在这种情况下将字符串转换为数组)。

文档:http://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm

【讨论】:

  • 您的说法并不完全正确。它与客户端发送的现代版本比服务器支持的情况有关。否则,服务器可能能够支持以前的格式。我更喜欢使用 C# 标准方式进行序列化,例如 ISerializable 接口。
【解决方案3】:

您可能想使用NewtonSoft.Json NuGET package

这是 .NET 社区中的一种标准。它也经常被称为Json.NET

你可以这样使用(example来自官网):

Product product = new Product();

product.Name = "Apple";
product.ExpiryDate = new DateTime(2008, 12, 28);
product.Price = 3.99M;
product.Sizes = new string[] { "Small", "Medium", "Large" };

string output = JsonConvert.SerializeObject(product);
//{
//  "Name": "Apple",
//  "ExpiryDate": "2008-12-28T00:00:00",
//  "Price": 3.99,
//  "Sizes": [
//    "Small",
//    "Medium",
//    "Large"
//  ]
//}

Product deserializedProduct = JsonConvert.DeserializeObject<Product>(output);

【讨论】:

  • 举一个我添加到问题中的示例,如何更改代码以处理不同的版本?
  • @LeoY 既然你已经改述了你的问题,我的回答已经不合时宜了。我认为序列化的方式并不重要,在这种情况下,重要的是如何实现版本控制。
  • 我在寻找某种隐含的支持。在这种情况下似乎没有,可能 NewtonSoft 的序列化程序最方便进行自定义实现,因为 ServiceStack.Text.JsonSerializer 太多面向 DTO。
【解决方案4】:

如果您愿意切换到 JSON.net,那么有一种更简单的方法。您不必使用包含versionBaseClass,也不必解析两次。诀窍是使用JObject,然后使用query JSON 作为version

JObject obj = JObject.Parse(json);
string version = obj.SelectToken("$.Version")?.ToString();

然后您可以像 Sándor 一样继续使用奖励部分,您可以使用 JObject 获取您的 dto 而不是重新读取 json:

ConditionsDto v1Dto = obj.ToObject<ConditionsDto>(readSerializer);

把它们放在一起:

public static ConditionsBusinessObject Parse(string json)
{
    JObject obj = JObject.Parse(json);
    string version = obj.SelectToken("$.Version")?.ToString();

    JsonSerializer readSerializer = JsonSerializer.CreateDefault(/*You might want to place your settings here*/);
    switch (version)
    {
        case null: //let's assume that there are some old files out there with no version at all
        //and that these are equivalent to the version 1
        case "1":
            ConditionsDto v1Dto = obj.ToObject<ConditionsDto>(readSerializer);
            if (v1Dto == null) return null; //or throw
            List<string> convertedConditions = new List<string> {v1Dto.Condition}; //See what I've done here?
            return new ConditionsBusinessObject(convertedConditions);
        case "2":
            ConditionsDtoV2 v2Dto = obj.ToObject<ConditionsDtoV2>(readSerializer);
            return v2Dto == null ? null //or throw
                : new ConditionsBusinessObject(v2Dto.Condition);
        default:
            throw new Exception($"Unsupported version {version}");
    }
}

以下是我拥有的课程供参考:

public class ConditionsDto
{
    public string Version { get; set; }
    public string Condition { get; set; }
}

public class ConditionsDtoV2
{
    public string Version { get; set; }
    public List<string> Condition { get; set; }
}

public class ConditionsBusinessObject
{

    public ConditionsBusinessObject(List<string> conditions)
    {
        Conditions = conditions;
    }

    public List<string> Conditions { get; }
}

还有几个测试来结束它:

    [Test]
    public void TestV1()
    {
        string v1 = @"{
    Version: ""1"",
    Condition: ""A < B""
}";

        //JsonHandler is where I placed Parse()
        ConditionsBusinessObject fromV1 = JsonHandler.Parse(v1);
        Assert.AreEqual(1, fromV1.Conditions.Count);
        Assert.AreEqual("A < B", fromV1.Conditions[0]);
    }
    [Test]
    public void TestV2()
    {
        string v2 = @"{
    Version: ""2"",
    Condition: [""A < B"", ""B = C"", ""B < 1""]
}";

        ConditionsBusinessObject fromV2 = JsonHandler.Parse(v2);
        Assert.AreEqual(3, fromV2.Conditions.Count);
        Assert.AreEqual("A < B", fromV2.Conditions[0]);
        Assert.AreEqual("B = C", fromV2.Conditions[1]);
        Assert.AreEqual("B < 1", fromV2.Conditions[2]);
    }

在一个普通的现实世界应用程序中,//See what I've done here? 部分是您必须完成所有转换工作的地方。我在那里没有做任何聪明的事情,我只是将单个 condition 包装到一个列表中,以使其与 current 业务对象兼容。正如您可能猜到的那样,随着应用程序的发展,这可能会爆炸。软件工程 SE 中的 This answer 在版本化 JSON 数据背后的理论中有更多详细信息,因此您可能需要查看一下以了解会发生什么。

关于读取到 JObject 然后转换为 dto 的性能影响的最后一句话是,我没有进行任何测量,但我预计它比解析两次要好。如果我发现这不是真的,我会相应地更新答案。

【讨论】:

    【解决方案5】:

    看看

    System.Web.Script.Serialization.JavaScriptSerializer
    

    样本

    var ser = new JavaScriptSerializer();
    var result = (IReadOnlyDictionary<string, object>)ser.DeserializeObject(json);
    
    if(result["version"] == "1.0")
    {
        // You expect a string for result["condition"]
    }
    else
    {
        // You expect an IEnumerable<string> for result["condition"]
    }
    

    【讨论】:

    • 举一个我添加到问题中的例子,这个类有什么帮助?
    • 问题是关于处理版本控制(即同一类型的不同模式)
    • 您可以使用 DeserializeObject(string) 方法管理不同的版本。我将发布一个示例。
    • 您建议我自己解析所有信息,同时我希望将这些信息完全放入类实现中以避免处理每个字段的解析
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-01-12
    • 1970-01-01
    • 1970-01-01
    • 2019-12-25
    • 2017-04-04
    • 2014-11-21
    • 1970-01-01
    相关资源
    最近更新 更多