Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/csharp/307.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 如何使用Newtonsoft JSON.NET处理JSON文档中的对象引用?_C#_.net_Json_Json.net_Deserialization - Fatal编程技术网

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

C# 如何使用Newtonsoft JSON.NET处理JSON文档中的对象引用?,c#,.net,json,json.net,deserialization,C#,.net,Json,Json.net,Deserialization,我有一个json数据集,它带有标准数据字段和引用字段。它看起来像这样: [ { "id":1, "name":"Book", "description":"Something you can read" }, { "id":2, "name":"newspaper", "description": { "ref":"0.description"

我有一个json数据集,它带有标准数据字段和引用字段。它看起来像这样:

[
    {
        "id":1,
        "name":"Book",
        "description":"Something you can read"
    },
    {
        "id":2,
        "name":"newspaper",
        "description": {
            "ref":"0.description"
        }
    }
]
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);
  }
}
这是我的数据模型:

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文件中的每个属性都有一个特定的类型,例如int表示id,string表示description,但是每个属性也可以通过ref链接到另一个属性。在这种情况下,id=2的描述与id=1相同

是否有一种方法可以通过错误处理或创建某种回退反序列化来应用,从而允许我序列化ref


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

您需要某种机制来解决这些引用

以下是两种方法:

1.使用内置的引用处理 其中一种机制是Newtonsoft serializer的属性,它与您描述的完全相同,只是它查找$id和$ref,而不是id和ref

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

然后,您可以使用内置的引用解析机制将此转换树转换为类型化对象,方法是使用获取转换树上的JsonReader,您可以使用它来指示它将其反序列化为所需的类型

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);
    }
  }
}
编辑:还可以查看哪个允许您从字符串或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了,所以请原谅我的语法错误或非惯用用法。但这是总体思路。

您需要一些机制来解决这些引用

以下是两种方法:

1.使用内置的引用处理 其中一种机制是Newtonsoft serializer的属性,它与您描述的完全相同,只是它查找$id和$ref,而不是id和ref

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

然后,您可以使用内置的引用解析机制将此转换树转换为类型化对象,方法是使用获取转换树上的JsonReader,您可以使用它来指示它将其反序列化为所需的类型

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);
    }
  }
}
编辑:还可以查看哪个允许您从字符串或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预加载到层次结构中,然后使用{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>>();
注:

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

它不处理对基本体的引用,只处理对象和数组

它不允许向前引用,只允许向后引用。这个问题不清楚JSON中的ref属性是否会引用文档后面的值

这些限制将使将问题中显示的引用语法转换为Json.NET语法变得复杂

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


演示小提琴。

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

以下扩展方法不适用于 他说:

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

// 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>>();
注:

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

它不处理对基本体的引用,只处理对象和数组

它不允许向前引用,只允许向后引用。这个问题不清楚JSON中的ref属性是否会引用文档后面的值

这些限制将使将问题中显示的引用语法转换为Json.NET语法变得复杂

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


演示小提琴。

您想要反序列化的数据模型是什么?如果您只是将描述声明为一个对象,那么Json.NET会将其反序列化为某种内容—字符串或JObject(视情况而定)。@dbc感谢您的回复。我将数据模型添加到原始帖子中。我知道我可以使用DeserializeAnonymousType对其进行反序列化,并将数据放入通用对象中。但是,我想将它直接输入到特定类型的对象中。如果只有一个通用对象,将描述转换为通用对象会有所帮助。。。然而,这是一个超级简单的例子,每个对象有30个字段,并且有很多级别,所有这些字段都可以是特定类型或引用。最后,我希望两个对象都有相同的字符串,但它们不必都指向内存中的同一字符串。只要object1.description和object2.description是包含您可以读取的内容的字符串,那么一切都将是完美的。如果您实际上不需要保留引用,并且可以克隆引用的值,那么您可以执行以下快速而肮脏的操作:。但这似乎与Avish的答案一致,因此我不确定是否有必要再添加一个。@dbc很抱歉,我正试图挤出时间来实施您的解决方案。如果你能回答这个问题,我会选择它。我能使你的小提琴符合比这个样品更复杂的实际情况。它包括不同类型的引用和引用数组等。最终,由于灵活性,这个解决方案对我来说是最好的。您想要反序列化的数据模型是什么?如果您只是将描述声明为一个对象,那么Json.NET会将其反序列化为某种内容—字符串或JObject(视情况而定)。@dbc感谢您的回复。我将数据模型添加到原始帖子中。我知道我可以使用DeserializeAnonymousType对其进行反序列化,并将数据放入通用对象中。但是,我想将它直接输入到特定类型的对象中。如果只有一个通用对象,将描述转换为通用对象会有所帮助。。。然而,这是一个超级简单的例子,每个对象有30个字段,并且有很多级别,所有这些字段都可以是特定类型或引用。最后,我希望两个对象都有相同的字符串,但它们不必都指向内存中的同一字符串。只要object1.description和object2.description是包含您可以读取的内容的字符串,那么一切都将是完美的。如果您实际上不需要保留引用,并且可以克隆引用的值,那么您可以执行以下快速而肮脏的操作:。但这似乎与Avish的答案一致,因此我不确定是否有必要再添加一个。@dbc很抱歉,我正试图挤出时间来实施您的解决方案。如果你能回答这个问题,我会选择它。我能使你的小提琴符合比这个样品更复杂的实际情况。它包括不同类型的引用和引用数组等。最终,这个解决方案对我来说是最好的,因为它具有灵活性。这非常有帮助,尽管比我希望的要痛苦一些。为了清晰起见,快速提问:transformedJsonTree实际上是jsonTree正确的吗?只是一个伪代码?关于Json.NET的PreserveReferencesHandling功能有两点:1它不处理对原语的引用,只处理对象和数组。2它不允许正向引用,只允许反向引用。OP的JSON中的ref属性是否会引用文档后面的值,这个问题并不清楚。如果他们这样做了,那么转换为$ref将不起作用,您需要对值进行重新排序,并使$id对象位于第一位。@KivakWolf是的,我在SO编辑器中写了这篇文章,所以希望出现错误和遗漏。文中描述了总体思路;这段代码只是为了说明这样的东西会是什么样子。@dbc谢谢,我不知道
那个在这种情况下,自产自销的方法可能更有意义。这非常有帮助,尽管比我希望的要痛苦一些。为了清晰起见,快速提问:transformedJsonTree实际上是jsonTree正确的吗?只是一个伪代码?关于Json.NET的PreserveReferencesHandling功能有两点:1它不处理对原语的引用,只处理对象和数组。2它不允许正向引用,只允许反向引用。OP的JSON中的ref属性是否会引用文档后面的值,这个问题并不清楚。如果他们这样做了,那么转换为$ref将不起作用,您需要对值进行重新排序,并使$id对象位于第一位。@KivakWolf是的,我在SO编辑器中写了这篇文章,所以希望出现错误和遗漏。文中描述了总体思路;这段代码只是为了说明这样的东西会是什么样子。@dbc谢谢,我不知道。在这种情况下,自滚方法可能更有意义。在我的回答中,使用LINQ到JSON查找ref对象比递归方法要好得多。但是请注意,您的解析逻辑尝试解析根文档的引用路径,而OP的场景有引用路径,其中第一个元素是文档中其他对象的$id,这是一个不同的用例。阅读我看到的其他注释可能误解了原始需求,所以OP必须澄清。除此之外,我们的答案基本相同。如果我的问题有点含糊,我道歉。我想要的是能够解析出引用并构建它,而不是使用一个封闭的系统,因为我的引用变量非常复杂,引用有时是ID,有时是元素,有时是混合物。虽然两个答案都是完全正确的,但我还是选择了这个答案,因为它让我能够很好地控制我需要的参考资料。除此之外,我还必须添加大量代码,但这是一个非常好的模板。使用LINQ to JSON查找ref对象比我的答案中的递归方法要好得多。但是请注意,您的解析逻辑尝试解析根文档的引用路径,而OP的场景有引用路径,其中第一个元素是文档中其他对象的$id,这是一个不同的用例。阅读我看到的其他注释可能误解了原始需求,所以OP必须澄清。除此之外,我们的答案基本相同。如果我的问题有点含糊,我道歉。我想要的是能够解析出引用并构建它,而不是使用一个封闭的系统,因为我的引用变量非常复杂,引用有时是ID,有时是元素,有时是混合物。虽然两个答案都是完全正确的,但我还是选择了这个答案,因为它让我能够很好地控制我需要的参考资料。除此之外,我还必须添加大量代码,但这就像一个伟大的锅炉板。