Newtonsoft.json:根据json路径白名单切JSON

Newtonsoft.json: cut JSON according to json path whitelist

假设,我有一些复杂的 JSON:

{
    "path1": {
        "path1Inner1": {
            "id": "id1"
        },
        "path1Inner2": {
            "id": "id2"
        }
    },
    "path2": {
        "path2Inner1": {
            "id": "id3"
        },
        "path2Inner2": {
            "id": "id4",
            "key": "key4"
        }
    }
}

还有一些json路径表达式的白名单,例如:

我想在 JSON 树中只留下与“白名单”匹配的节点和属性,因此,结果将是:

{
    "path1": {
        "path1Inner1": {
            "id": "id1"
        }
    },
    "path2": {
        "path2Inner2": {
            "key": "key4"
        }
    }
}

即这不仅仅是 JSON 路径的选择(这是一项微不足道的任务),而且节点和属性必须保持源 JSON 树中的初始位置。

假设您在 sample.json 文件中有一个有效的 json:

{
  "path1": {
    "path1Inner1": {
      "id": "id1"
    },
    "path1Inner2": {
      "id": "id2"
    }
  },
  "path2": {
    "path2Inner1": {
      "id": "id3"
    },
    "path2Inner2": {
      "id": "id4",
      "key": "key4"
    }
  }
}

然后你可以用下面的程序实现你想要的输出:

static void Main()
{
    var whitelist = new[] { "$.path1.path1Inner1", "$.path2.path2Inner2.key" };
    var rawJson = File.ReadAllText("sample.json");

    var semiParsed = JObject.Parse(rawJson);
    var root = new JObject();

    foreach (var path in whitelist)
    {
        var value = semiParsed.SelectToken(path);
        if (value == null) continue; //no node exists under the path 
        var toplevelNode = CreateNode(path, value);
        root.Merge(toplevelNode);
    }

    Console.WriteLine(root);
}
  1. 我们读取 json 文件并将其半解析为 JObject
  2. 我们定义一个root where 将合并处理结果
  3. 我们遍历列入白名单的 json 路径来处理它们
  4. 我们通过 SelectToken 调用
  5. 检索节点(由路径指定)的实际 value
  6. 如果路径指向不存在的节点,则 SelectToken returns null
  7. 然后我们创建一个新的 JObject,其中包含完整的层次结构和检索到的值
  8. 最后我们将该对象合并到 root

现在让我们看看这两个辅助方法

static JObject CreateNode(string path, JToken value)
{
    var entryLevels = path.Split('.').Skip(1).Reverse().ToArray();
    return CreateHierarchy(new Queue<string>(entryLevels), value);
}
  1. 我们用点分割路径并删除第一个元素($
  2. 我们颠倒顺序以便能够将其放入队列
  3. 我们想建立由内而外的层次结构
  4. 最后我们用队列和检索到的值调用递归函数
static JObject CreateHierarchy(Queue<string> pathLevels, JToken currentNode)
{
    if (pathLevels.Count == 0) return currentNode as JObject;

    var newNode = new JObject(new JProperty(pathLevels.Dequeue(), currentNode));
    return CreateHierarchy(pathLevels, newNode);
}
  1. 我们首先定义退出条件以确保我们不会创建无限递归
  2. 我们创建一个新的 JObject 并在其中指定名称和值

程序的输出如下:

{
  "path1": {
    "path1Inner1": {
      "id": "id1"
    }
  },
  "path2": {
    "path2Inner2": {
      "key": "key4"
    }
  }
}

首先,非常感谢 and 的回答。它们成为我分析问题的出发点。

这些答案提供了两种不同的方法来实现按路径“列入白名单”的目标。 one rebuilds the whitelist paths structure from scratch (i.e. starting from the empty object creates the needed routes). The implementation parses the string paths and tries to rebuild the tree based on the parsed path. This approach needs very handy work of considering all possible types of paths and therefore might be error-prone. You can find some of the mistakes I have found in my comment to the .

方法基于 json.net 对象树 API(父、祖先、后代等)。该算法遍历树并删除未列入“白名单”的路径。我发现这种方法更容易,更不容易出错,并且“一次性”支持广泛的案例。

我实现的算法在很多方面与 的答案相似,但我认为在实现和理解方面要容易得多。另外,我不认为它的性能更差。

    public static class JsonExtensions
    {
        public static TJToken RemoveAllExcept<TJToken>(this TJToken token, IEnumerable<string> paths) where TJToken : JContainer
        {
            HashSet<JToken> nodesToRemove = new(ReferenceEqualityComparer.Instance);
            HashSet<JToken> nodesToKeep = new(ReferenceEqualityComparer.Instance);

            foreach (var whitelistedToken in paths.SelectMany(token.SelectTokens))
                TraverseTokenPath(whitelistedToken, nodesToRemove, nodesToKeep);

            //In that case neither path from paths has returned any token
            if (nodesToKeep.Count == 0)
            {
                token.RemoveAll();
                return token;
            }

            nodesToRemove.ExceptWith(nodesToKeep);

            foreach (var notWhitelistedNode in nodesToRemove)
                notWhitelistedNode.Remove();

            return token;
        }

        private static void TraverseTokenPath(JToken value, ISet<JToken> nodesToRemove, ISet<JToken> nodesToKeep)
        {
            JToken? immediateValue = value;

            do
            {
                nodesToKeep.Add(immediateValue);

                if (immediateValue.Parent is JObject or JArray)
                {
                    foreach (var child in immediateValue.Parent.Children())
                        if (!ReferenceEqualityComparer.Instance.Equals(child, value))
                            nodesToRemove.Add(child);
                }

                immediateValue = immediateValue.Parent;
            } while (immediateValue != null);
        }
    }

要比较 JToken 实例,有必要使用引用相等性比较器,因为某些 JToken 类型使用“按值”比较,就像 JValue 所做的那样。否则,在某些情况下您可能会遇到错误行为。

例如,有源JSON

{
   "path2":{
      "path2Inner2":[
         "id",
         "id"
      ]
   }
}

和路径 $..path2Inner2[0] 你会得到结果 JSON

{
   "path2":{
      "path2Inner2":[
         "id",
         "id"
      ]
   }
}

而不是

{
   "path2":{
      "path2Inner2":[
         "id"
      ]
   }
}

就 .net 5.0 而言,可以使用标准 ReferenceEqualityComparer。如果您使用早期版本的 .net,您可能需要 implement it.