如何递归地降低System.Text.Json JsonNode层次结构(等效于Json.NET的JToken.DescendantsAndSelf())?

qkf9rpyu  于 2023-02-26  发布在  .NET
关注(0)|答案(1)|浏览(134)

我有一个任意的JSON文档(即没有预先知道的固定模式),我想递归地向下搜索文档中任何级别上与某个 predicate 匹配的所有节点,以便进行一些必要的修改。如何使用JsonNode文档对象模型执行这样的递归搜索?
具体内容如下。
假设我有一些JSON,如下面这样,其中可能包含属性"password"的一个或多个示例:

[
  {
    "column1": "val_column1",
    "column2": "val_column2",
    "sheet2": [
      {
        "sheet2col1": "val_sheet2column1",
        "sheet3": [
          {
            "sheet3col1": "val_sheet3column1",
            "password": "password to remove"
          }
        ]
      },
      {
        "sheet2col1": "val_sheet2column1",
        "sheet3": [
          {
            "sheet3col1": "val_sheet3column1"
          }
        ]
      }
    ]
  },
  {
    "column1": "val2_column1",
    "column2": "val2_column2",
    "password": "password to remove",
    "sheet2": [
      {
        "sheet2col1": "val_sheet2column1",
        "sheet3": [
          {
            "sheet3col2": "val_sheet3column2"
          },
          null,
          null,
          19191
        ],
        "password": "password to remove"
      },
      {
        "sheet2col1": "val_sheet2column1",
        "sheet3": [
          {
            "sheet3col2": "val_sheet3column2"
          }
        ]
      }
    ]
  }
]

我需要将其解析为JsonNode层次结构,并删除所有“password”属性(无论它们出现在JSON层次结构中的何处)。使用Json.NET,我可以将其解析为JToken并使用DescendantsAndSelf()

var root = JToken.Parse(json);
var propertyToRemove = "password";
if (root is JContainer c)
    foreach (var obj in c.DescendantsAndSelf().OfType<JObject>().Where(o => o.ContainsKey(propertyToRemove)))
        obj.Remove(propertyToRemove);
var newJson = root.ToString();

但是JsonNode没有一个等价的方法,我如何使用System.Text.json来完成这个任务呢?

vq8itlhq

vq8itlhq1#

由于JsonNode没有与DescendantsAndSelf()等效的函数,因此我们必须自己创建一个:

public static partial class JsonExtensions
{
    public static IEnumerable<JsonNode?> Descendants(this JsonNode? root) => root.DescendantsAndSelf(false);

    /// Recursively enumerates all JsonNodes in the given JsonNode object in document order.
    public static IEnumerable<JsonNode?> DescendantsAndSelf(this JsonNode? root, bool includeSelf = true) => 
        root.DescendantItemsAndSelf(includeSelf).Select(i => i.node);
    
    /// Recursively enumerates all JsonNodes (including their index or name and parent) in the given JsonNode object in document order.
    public static IEnumerable<(JsonNode? node, int? index, string? name, JsonNode? parent)> DescendantItemsAndSelf(this JsonNode? root, bool includeSelf = true) => 
        RecursiveEnumerableExtensions.Traverse(
            (node: root, index: (int?)null, name: (string?)null, parent: (JsonNode?)null),
            (i) => i.node switch
            {
                JsonObject o => o.AsDictionary().Select(p => (p.Value, (int?)null, p.Key.AsNullableReference(), i.node.AsNullableReference())),
                JsonArray a => a.Select((item, index) => (item, index.AsNullableValue(), (string?)null, i.node.AsNullableReference())),
                _ => i.ToEmptyEnumerable(),
            }, includeSelf);
    
    static IEnumerable<T> ToEmptyEnumerable<T>(this T item) => Enumerable.Empty<T>();
    static T? AsNullableReference<T>(this T item) where T : class => item;
    static Nullable<T> AsNullableValue<T>(this T item) where T : struct => item;
    static IDictionary<string, JsonNode?> AsDictionary(this JsonObject o) => o;
}

public static partial class RecursiveEnumerableExtensions
{
    // Rewritten from the answer by Eric Lippert https://stackoverflow.com/users/88656/eric-lippert
    // to "Efficient graph traversal with LINQ - eliminating recursion" http://stackoverflow.com/questions/10253161/efficient-graph-traversal-with-linq-eliminating-recursion
    // to ensure items are returned in the order they are encountered.
    public static IEnumerable<T> Traverse<T>(
        T root,
        Func<T, IEnumerable<T>> children, bool includeSelf = true)
    {
        if (includeSelf)
            yield return root;
        var stack = new Stack<IEnumerator<T>>();
        try
        {
            stack.Push(children(root).GetEnumerator());
            while (stack.Count != 0)
            {
                var enumerator = stack.Peek();
                if (!enumerator.MoveNext())
                {
                    stack.Pop();
                    enumerator.Dispose();
                }
                else
                {
                    yield return enumerator.Current;
                    stack.Push(children(enumerator.Current).GetEnumerator());
                }
            }
        }
        finally
        {
            foreach (var enumerator in stack)
                enumerator.Dispose();
        }
    }
}

而现在我们将能够做到:

var root = JsonNode.Parse(json);

var propertyToRemove = "password";
foreach (var obj in root.DescendantsAndSelf().OfType<JsonObject>().Where(o => o.ContainsKey(propertyToRemove)))
    obj.Remove(propertyToRemove);

var options = new JsonSerializerOptions { WriteIndented = true /* Use whatever you want here */ };
var newJson = JsonSerializer.Serialize(root, options);

演示小提琴#1 here
请记住与Json.NET的LINQ to JSON的以下区别:
1.为null JSON值返回的JsonNode(例如{"value":null}实际上为空。LINQ to JSON将null JSON值表示为非空JValue,其中JValue.Type等于JTokenType.Null

  1. JsonNode没有任何等价于Json.NET的JProperty的东西。对象中值的parent将是对象本身。因此,没有直接的方法通过JsonNode文档对象模型来确定所选JsonNode属性值的属性名。
    因此,如果你需要通过值(而不是名称)搜索和修改属性,你可以使用第二个扩展方法DescendantItemsAndSelf(),它包括父节点和名称或索引沿着当前节点。例如,要删除所有null属性值,请执行以下操作:
foreach (var item in root.DescendantItemsAndSelf().Where(i => i.name != null && i.node == null).ToList())
    ((JsonObject)item.parent!).Remove(item.name!);

演示小提琴#2。

相关问题