【问题标题】:Newtonsoft.json: cut JSON according to json path whitelistNewtonsoft.json:根据json路径白名单剪切JSON
【发布时间】:2021-11-30 21:12:35
【问题描述】:

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

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

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

  • $.path1.path1Inner1
  • $.path2.path2Inner2.key

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

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

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

【问题讨论】:

标签: json.net


【解决方案1】:

假设您在 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 来合并处理结果
  3. 我们遍历列入白名单的 json 路径来处理它们
  4. 我们通过SelectToken调用检索节点的实际value(由路径指定)
  5. 如果路径指向不存在的节点,则SelectToken 返回null
  6. 然后我们创建一个新的JObject,其中包含完整的层次结构和检索到的值
  7. 最后我们将该对象合并到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"
    }
  }
}

【讨论】:

  • Peter Csala,非常感谢您尝试解决我的问题以及您为实施解决方案所花费的时间。但实际上存在一些问题。首先,jsonpath 语法允许使用大量不同类型的路径,例如 "$..id" 、 "$..[?(@.id)]" 等等。这是一项非常艰巨的任务正确解析它们。因此,将 value.Path 发送到 CreateNode 要容易得多。但是,无论如何,还有一些其他情况没有考虑到,例如“$.path[0]”。
  • 另外,如果您不是基于现有树而是尝试重新创建它,您可能会遇到一些其他难以修复的问题。例如,有两个白名单路径“$.path[0]”和“$.path[5]”,很难在白名单“path”数组中保留项目 0 和项目 5 的相对位置。所有这些问题使我认为依赖源树并删除未列入白名单的节点要容易得多。这就是我在回答中实际所做的。
  • @ademchenko 我的实现正如你所说,它不是防弹的。通过向您展示一些基本概念,我向您展示了如何开始构建自己的选项。因为在您的问题中您没有指定解决方案的通用性,这就是为什么我实施了一个适用于所提供示例的解决方案。
  • @ademchenko 反思您的第二条评论:您的示例再次非常简单,没有详细的要求。因此,显然我无法提供适用于每种情况的深刻解决方案。我很高兴您已经构建了自己的解决方案来满足您的所有需求。
【解决方案2】:

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

这些答案提供了两种不同的方法来实现按路径“列入白名单”的目标。 first 从头开始​​重建白名单路径结构(即从空对象开始创建所需的路由)。该实现解析字符串路径并尝试基于解析的路径重建树。这种方法需要非常方便的工作来考虑所有可能的路径类型,因此可能容易出错。您可以在我对answer 的评论中找到我发现的一些错误。

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

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

    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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-11-04
    • 1970-01-01
    • 2018-01-10
    • 2018-07-18
    • 1970-01-01
    • 2019-11-13
    • 2019-10-18
    相关资源
    最近更新 更多