使用 Newtonsoft.JSON 自定义转换器读取具有不同输入的 json

Using Newtonsoft.JSON custom converters to read json with different input

我正在使用 NewtonSoft.Json 来 read/write 我们的数据 json。一个(非常简化的)例子是:

{
  "$type": "MyNamespace.LandingEvent, MyAssembly",
  "TimeOfLanding": "2021-04-11T15:00:00.000Z",
  "AirportName": "KLAX",
  "AirportRunway": "25L"
}

使用模仿属性的 C# DTO class。请注意,我们使用 TypeNameHandling.

我们想将 C# class 更改为更复杂的设置:

class Airport
{
    public string Name { get; set; }
    public string Runway { get; set; }
}

class LandingEvent
{
    public DateTime TimeOfLanding { get; set; }
    public Airport Airport { get; set; }
}

这将导致新数据将写入 JSON 为:

{
  "$type": "MyNamespace.LandingEvent, MyAssembly",
  "TimeOfLanding": "2021-04-11T15:00:00.000Z",
  "Airport": {
    "Name": "KLAX",
    "Runway": "25L"
  }
}

但我们仍然需要能够读取旧的 JSON 数据并解析为新的 class 结构。 这就是我目前的工作奋斗.

我知道要走的路可能是专门的 JsonConverter。在这方面我有几个问题:

  1. 如何读取 $type 属性 并实例化正确的类型? (我的重写 CanConvert() 方法被提供了一个 base-class 的名称(由于实际上下文比这个例子更复杂)。
  2. 如果 属性 AirportName 存在,我只想进行自定义读取。如果不是这种情况,我该如何回退到默认反序列化?

编辑:需要进行一些说明。如果我创建自定义 JsonConverter,那么 CanConvert 将接收类型 EventBase,但 $type 实际上可以包含 "MyNamespace.LandingEvent, MyAssembly" "MyNamespace.TakeoffEvent, MyAssembly"。因此,我可能需要根据这个值自己实例化返回的对象。不过,我不确定如何。

类 变化,这种 Json 字符串变化,将在未来的版本中获得额外的功能。你会不断调整你的声明。使用 Newtonsoft,您可以为不同的 class 继承添加自定义处理程序并继续使用反序列化,但您必须维护该代码。

对于动态 Json,我发现使用 JObject、JArray 和 JToken 更容易自由地解析 Json 字符串。特别是如果您只对某些领域感兴趣。

我只能给你举个例子,我觉得这和你的项目有(一点点)关系,但不是同一个部分(笑脸)

我使用以下代码解码 Blender 生成的 MSFS 转换格式的 glTF 3d 对象文件的一部分。这种 Json-like 格式由 部分 组成。每个 Json 部分看起来像这样,

  "asset" : {
    "extensions" : {
        "ASOBO_normal_map_convention" : {
            "tangent_space_convention" : "DirectX"
        }
    },
    "generator" : "Extended Khronos glTF Blender I/O v1.0.0",
    "version" : "2.0"
  },

.. 但这些部分及其字段大多是可选的,并且在某些 GLtf 中它们未填写。它不是 classes 的“可序列化或反序列化”。

我声明一些

    public JObject AssetObject;

..从Json字符串sJson中填充如下:

        dynamic stuff = JObject.Parse(sJson);
        var pp = stuff.Children();
        Dictionary<string, bool> d = new Dictionary<string, bool>();
        foreach (JProperty jo in pp) d[jo.Name] = true; // all sections

        string cSection= "asset";
        if (!d.ContainsKey(cSection)) { LogLine(98, "Warning: BPG Json has no " + cSection + " section."); return false; }
        else
        {
            AssetObject = (JObject)stuff[cSection];
            ParseGLBAsset();
        }

首先注意动态声明的使用,一个部分将通过强制转换落入JObject。我将该部分的各个部分存储到字符串属性中。解析本身发生在 ParseGLBAsset() 中,此函数如下所示:

    public void ParseGLBAsset()
    {
        foreach (JProperty jbp in AssetObject.Children())
            if (jbp.Name == "generator")
            { GLBGenerator = jbp.Value.ToString(); }
            else
            if (jbp.Name == "extensions")
            {
                GLBAssetExtensions = jbp.Value.ToString(); 
                LogLine(0, "Asset extensions: " + GLBAssetExtensions);
            }
            else
              if (jbp.Name == "version")
            { GLBVersion = jbp.Value.ToString(); }
        LogLine(1, "Found asset.generator=" + GLBGenerator);
        LogLine(1, "Found asset.version=" + GLBVersion);

    }

您可以使用自定义 JsonConverter 来处理多态事件类型和变化的 JSON 格式。下面是一个例子。它通过将数据加载到 JObject 来工作,它可以在其中读取 $type 属性 并实例化正确的事件类型。从那里,它将尝试从 JSON 填充事件对象。如果 Airport 反序列化失败,它将尝试读取遗留机场属性并从中填充一个新的 Airport 实例。

class EventConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(BaseEvent).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject obj = JObject.Load(reader);

        string type = (string)obj["$type"];
        BaseEvent baseEvent;
        if (type.Contains(nameof(TakeoffEvent)))
        {
            baseEvent = new TakeoffEvent();
        }
        else
        {
            baseEvent = new LandingEvent();
        }
        serializer.Populate(obj.CreateReader(), baseEvent);

        if (baseEvent.Airport == null)
        {
            baseEvent.Airport = new Airport
            {
                Name = (string)obj["AirportName"],
                Runway = (string)obj["AirportRunway"]
            };
        }
        return baseEvent;
    }

    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

注意:这假设您的 class 结构实际如下所示:

class Airport
{
    public string Name { get; set; }
    public string Runway { get; set; }
}

class BaseEvent
{
    public Airport Airport { get; set; }
}

class TakeoffEvent : BaseEvent
{
    public DateTime TimeOfTakeoff { get; set; }
}

class LandingEvent : BaseEvent
{
    public DateTime TimeOfLanding { get; set; }
}

要使用转换器,请将其添加到 JsonSerializerSettings 中的 Converters 集合,然后将设置传递给 DeserializeObject():

var settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects,
    Converters = new List<JsonConverter> { new EventConverter() }
};

var baseEvent = JsonConvert.DeserializeObject<BaseEvent>(json, settings);

这是一个工作演示:https://dotnetfiddle.net/jSaq4T

另请参阅:Adding backward compatibility support for an older JSON structure