繁体   English   中英

如何使用 Newtonsoft Json.NET 处理 JSON 文档中的对象引用?

[英]How to handle object references in JSON document with Newtonsoft Json.NET?

我有一个带有标准数据字段和参考字段的 json 数据集。 它看起来像这样:

[
    {
        "id":1,
        "name":"Book",
        "description":"Something you can read"
    },
    {
        "id":2,
        "name":"newspaper",
        "description": {
            "ref":"0.description"
        }
    }
]

这是我的数据模型:

public class PhysicalObject {
    [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default)]
    public int id;

    [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default)]
    public string name;

    [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default)]  // FIXED should have been description not desc
    public string desc;
}

json 文件中的每个属性都有一个特定的类型,例如idintdescriptionstring ,但是每个属性也可以通过ref链接到另一个属性。 在这种情况下, descriptionid = 2是相同的id = 1

有没有办法通过错误处理或创建某种可以应用的回退反序列化来序列化ref

请注意,由于其他要求,我必须使用 Newtonsoft Json.NET 库来解决此问题。 有关解决此问题的其他库或技术的信息是有用的,但可能无法解决问题。

您将需要某种机制来解析这些引用。

这里有两种方法:

1. 使用内置的引用处理

一种这样的机制是 Newtonsoft 序列化程序的PreserveReferencesHandling属性,它完全符合您的描述,除了它查找$id$ref而不是idref

要使用它,您可以在将 JSON 树转换为类型化对象之前对其进行转换,首先将其读入 JSON 树表示(使用JToken.Parse ),然后遍历该树,用$id$ref替换idref属性(由于中间 JSON 树本质上是可修改和动态的,因此您可以轻松完成此操作)。

然后,您可以使用内置的引用解析机制将此转换后的树转换为您的类型化对象,方法是使用JObject.CreateReader在转换后的树上获取JsonReader ,您可以将其提供给JsonSerializer.Deserialize<T>以指示它反序列化它变成你想要的类型。

T DeserializeJsonWithReferences<T>(string input) 
{
  var jsonTree = JToken.Parse(jsonString);
  TransformJsonTree(jsonTree);  // renames `id` and `ref` properties in-place
  var jsonReader = jsonTree.CreateReader();
  var jsonSerializer = new JsonSerializer() { 
    PreserveReferencesHandling = PreserveReferenceHandling.All 
  };
  var deserialized = jsonSerializer.Deserialize<T>(jsonReader);
  return deserialized;
}

void TransformJsonTree(JToken token)
{
  var container = token as JContainer;
  if (container == null) 
    return;

  foreach (propName in SpecialPropertyNames)  // {"id", "ref"}
  {
    objects = container
      .Descendants()
      .OfType<JObject>()
      .Where(x => x.ContainsKey(propName));

    foreach (obj in objects) 
    {
      obj["$" + propName] = obj[propName];
      obj.Remove(propName);
    }
  }
}

2.滚动你自己的参考分辨率层

一个更复杂的方法,如果你想自己做:你需要添加你自己的参考解析层,它会在将 JSON 树转换为类型化对象之前对其进行转换。

同样,您可以首先将 JSON 流读入 JSON 树表示形式。 然后你需要遍历那棵树两次:

  • 在第一次遍历时,您将查找具有id属性的对象,并将它们记录在字典中(从id到包含它的对象)。

  • 在第二次遍历中,您将查找具有ref属性的对象,并将这些 ref 对象替换为适当的值,方法是在您之前创建的字典中通过id查找被引用的对象,然后根据该属性导航其属性ref值中描述的链。 例如,如果 ref 是3.address.city ,您将查找 ID 为 3 的对象,然后找到它的address属性的值,然后找到该值的city属性的值,这就是参考的最终值。

一旦 JSON 树被转换并且所有引用对象都被它们对应的引用值替换,您就可以将 JSON 树转换为类型化对象。

代码方面与前面的示例完全相同,除了在transformJsonTree ,您不仅要重命名idref属性,还必须实现实际的查找和引用解析逻辑。

它可能看起来像这样:

IDictionary<string, JToken> BuildIdMap(JContainer container)
{
  return container
    .Descendants()
    .OfType<JObject>()
    .Where(obj => obj.ContainsKey(IdPropertyName)
    .ToDictionary(obj => obj[IdPropertyName], obj => obj);
}

JToken LookupReferenceValue(string referenceString, IDictionary<string, JObject> idToObjectMap)
{
  var elements = referenceString.Split('.');
  var obj = idToObjectMap(elements[0]);

  for (int i = 1; i < elements.Length; i++) 
  {
    var elem = elements[i];
    switch (obj) 
    {
      case JArray jarr:
        obj = arr[elem];  // elem is a property name
        break;
      case JObject jobj:
        obj = jobj[int.Parse(elem)];  // elem is an array index
        break;
      default:
        throw Exception("You should throw a meaningful exception here"); 
    }
  }
}

void ResolveReferences(JContainer container, IDictionary<string, JObject> idToObjectMap)
{
  refObjects = container
    .Descendants()
    .OfType<JObject>()
    .Where(obj.Count == 1 && obj => obj.ContainsKey(RefPropertyName))

  foreach (var refObject in refObjects) 
  {
    referenceString = refObject[RefPropertyName];
    referencedValue = LookupReferenceValue(refObject, idToObjectMap)
    refObject.Replace(referencedValue);
  }
}

编辑:另请看一下JToken.SelectToken ,它允许您从字符串或JsonPath导航属性链,从JsonPath节省了很多麻烦(假设您的文档中的参考语法与 Newtonsoft 支持的语法相匹配,例如关于数组索引)。

JToken LookupReferenceValue(string referenceString, IDictionary<string, JObject> idToObjectMap)
{
  var parts = referenceString.Split('.', 1); // only split on first '.'
  var id = parts[0];
  var tokenPath = parts[1];
  var referencedObject = idToObjectMap[id];
  var referencedValue = referencedObject.SelectToken(tokenPath);
  return referencedValue;
}

我已经好几年没有写任何 C# 了,所以请原谅任何语法错误或非惯用语。 但这是一般的想法。

您可以将 JSON 预加载到JToken层次结构中,然后使用LINQ to JSON{"ref":"some.period-separated.path"}形式的对象替换为路径中指示的标记。 然后随后JToken层次结构可以反序列化为您的最终模型。

以下扩展方法可以解决问题:

public static partial class JsonExtensions
{
    const string refPropertyName = "ref";

    public static void ResolveRefererences(JToken root)
    {
        if (!(root is JContainer container))
            return;
        var refs = container.Descendants().OfType<JObject>().Where(o => IsRefObject(o)).ToList();
        Console.WriteLine(JsonConvert.SerializeObject(refs));
        foreach (var refObj in refs)
        {
            var path = GetRefObjectValue(refObj);
            var original = ResolveRef(root, path);
            if (original != null)
                refObj.Replace(original);
        }
    }

    static bool IsRefObject(JObject obj)
    {
        return GetRefObjectValue(obj) != null;
    }

    static string GetRefObjectValue(JObject obj)
    {
        if (obj.Count == 1)
        {
            var refValue = obj[refPropertyName];
            if (refValue != null && refValue.Type == JTokenType.String)
            {
                return (string)refValue;
            }
        }
        return null;
    }

    static JToken ResolveRef(JToken token, string path)
    {
        // TODO: determine whether it is possible for a property name to contain a '.' character, and if so, how the path will look.
        var components = path.Split('.'); 

        foreach (var component in components)
        {
            if (token is JObject obj)
                token = obj[component];
            else if (token is JArray array)
                token = token[int.Parse(component, NumberFormatInfo.InvariantInfo)];
            else
                // Or maybe just return null?
                throw new JsonException("Unexpected token type.");
        }
        return token;
    }
} 

然后您将按如下方式使用它:

// Load into intermediate JToken hierarchy; do not perform DateTime recognition yet.
var root = JsonConvert.DeserializeObject<JToken>(jsonString, new JsonSerializerSettings { DateParseHandling = DateParseHandling.None });

// Replace {"ref": "...") objects with their references.
JsonExtensions.ResolveRefererences(root);

// Deserialize directly to final model.  DateTime recognition should get performed now.
var list = root.ToObject<List<PhysicalObject>>();

笔记:

  1. 此解决方案不会尝试保留引用,即使反序列化的{"ref":"some.period-separated.path"}引用与反序列化的原始实例相同的实例。 虽然 Json.NET 确实具有通过"$ref""$id"属性保留对象引用的功能,但它有几个限制,包括:

    • 它不处理对原语的引用,只处理对象和数组。

    • 它不允许前向引用,只允许后向引用。 从问题中不清楚 JSON 中的"ref"属性是否可能引用文档后面的值。


    这些限制会使问题中显示的参考语法转换为 Json.NET 的语法变得复杂。

  2. DateTime识别推迟到最终反序列化是个好主意。 如果您的模型具有其 JSON 值可能看起来像 ISO 8601 日期的string属性,那么过早的日期识别可能会导致字符串值被修改。

演示小提琴在这里

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM