pcd*_*dev 6 c# json.net jsonpath
我有一个任意定义的JSON文档,并且希望能够对属性应用白名单过滤器之类的JSONPath表达式:保留所有选定节点及其祖先回到根节点,所有其他节点均被删除。如果节点不存在,我应该以空文档结尾。
似乎没有与JSON.Net中内置的类似的东西,并且我在任何地方都找不到类似的示例,所以我建立了自己的示例。我选择将选定的节点复制到新建文档中,而不是尝试删除所有不匹配的节点。鉴于可能存在多个匹配项并且文档可能很大,因此需要能够有效地将多个选择结果合并到单个树/ JSON文档中。
我的尝试有点奏效,但结果却很奇怪。该过程涉及一种MergedAncestry对SelectTokens结果进行迭代,调用的方法GetFullAncestry(将其递归地构建到该节点的树),然后合并结果。不过,似乎JArrays的合并发生在错误的级别,如下面的“实际结果”所示。
我的问题:
码:
public static void Main()
{
string json = @"..."; // snipped for brevity - see DotNetFiddle: https://dotnetfiddle.net/wKN1Hj
var root = (JContainer)JToken.Parse(json);
var t3 = root.SelectTokens("$.Array3B.[*].Array3B1.[*].*");
// See DotNetFiddle for simpler examples that work
Console.WriteLine($"{MergedAncestry(t3).ToString()}"); // Wrong output!
Console.ReadKey();
}
// Returns a single document merged using the full ancestry of each of the input tokens
static JToken MergedAncestry(IEnumerable<JToken> tokens)
{
JObject merged = null;
foreach(var token in tokens)
{
if (merged == null)
{
// First object
merged = (JObject)GetFullAncestry(token);
}
else
{
// Subsequent objects merged
merged.Merge((JObject)GetFullAncestry(token), new JsonMergeSettings
{
// union array values together to avoid duplicates
MergeArrayHandling = MergeArrayHandling.Union
});
}
}
return merged ?? new JObject();
}
// Recursively builds a new tree to the node matching the ancestry of the original node
static JToken GetFullAncestry(JToken node, JToken tree = null)
{
if (tree == null)
{
// First level: start by cloning the current node
tree = node?.DeepClone();
}
if (node?.Parent == null)
{
// No parents left, return the tree we've built
return tree;
}
// Rebuild the parent node in our tree based on the type of node
JToken a;
switch (node.Parent)
{
case JArray _:
return GetFullAncestry(node.Parent, new JArray(tree));
case JProperty _:
return GetFullAncestry(node.Parent, new JProperty(((JProperty)node.Parent).Name, tree));
case JObject _:
return GetFullAncestry(node.Parent, new JObject(tree));
default:
return tree;
}
}
Run Code Online (Sandbox Code Playgroud)
JSON示例:
{
"Array3A": [
{ "Item_3A1": "Desc_3A1" }
],
"Array3B": [
{ "Item_3B1": "Desc_3B1" },
{
"Array3B1": [
{ "Item_1": "Desc_3B11" },
{ "Item_2": "Desc_3B12" },
{ "Item_3": "Desc_3B13" }
]
},
{
"Array3B2": [
{ "Item_1": "Desc_3B21" },
{ "Item_2": "Desc_3B22" },
{ "Item_3": "Desc_3B23" }
]
}
]
}
Run Code Online (Sandbox Code Playgroud)
有关完整的代码和测试,请参见DotNetFiddle。
“过滤器” JSONPath:
$.Array3B.[*].Array3B1.[*].*
Run Code Online (Sandbox Code Playgroud)
预期成绩:
{
"Array3B": [
{
"Array3B1": [
{ "Item_1": "Desc_3B11" },
{ "Item_2": "Desc_3B12" },
{ "Item_3": "Desc_3B13" }
]
}
]
}
Run Code Online (Sandbox Code Playgroud)
实际结果:
{
"Array3B": [
{
"Array3B1": [ { "Item_1": "Desc_3B11" } ]
},
{
"Array3B1": [ { "Item_2": "Desc_3B12" } ]
},
{
"Array3B1": [ { "Item_3": "Desc_3B13" } ]
}
]
}
Run Code Online (Sandbox Code Playgroud)
好吧,我已经找到办法了。感谢@dbc 的建议、改进和指出问题。
递归最终不会很好地工作,因为我需要确保树中具有公共父级的同一级别的所有节点都将匹配,而任何级别都可能有输入节点。
我添加了一个方法来对多个 JSONPath 进行过滤以输出单个结果文档,因为这是最初的目标。
static JToken FilterByJSONPath(JToken document, IEnumerable<string> jPaths)
{
var matches = jPaths.SelectMany(path => document.SelectTokens(path, false));
return MergeAncestry(matches);
}
static JToken MergeAncestry(IEnumerable<JToken> tokens)
{
if (tokens == null || !tokens.Any())
{
return new JObject();
}
// Get a dictionary of tokens indexed by their depth
var tokensByDepth = tokens
.Distinct(ObjectReferenceEqualityComparer<JToken>.Default)
.GroupBy(t => t.Ancestors().Count())
.ToDictionary(
g => g.Key,
g => g.Select(node => new CarbonCopyToken { Original = node, CarbonCopy = node.DeepClone() })
.ToList());
// start at the deepest level working up
int depth = tokensByDepth.Keys.Max();
for (int i = depth; i > 0; i--)
{
// If there's nothing at the next level up, create a list to hold parents of children at this level
if (!tokensByDepth.ContainsKey(i - 1))
{
tokensByDepth.Add(i - 1, new List<CarbonCopyToken>());
}
// Merge all tokens at this level into families by common parent
foreach (var parent in MergeCommonParents(tokensByDepth[i]))
{
tokensByDepth[i - 1].Add(parent);
}
}
// we should be left with a list containing a single CarbonCopyToken - contining the root of our copied document and the root of the source
var cc = tokensByDepth[0].FirstOrDefault();
return cc?.CarbonCopy ?? new JObject();
}
static IEnumerable<CarbonCopyToken> MergeCommonParents(IEnumerable<CarbonCopyToken> tokens)
{
var newParents = tokens.GroupBy(t => t.Original.Parent).Select(g => new CarbonCopyToken {
Original = g.First().Original.Parent,
CarbonCopy = CopyCommonParent(g.First().Original.Parent, g.AsEnumerable())
});
return newParents;
}
static JToken CopyCommonParent(JToken parent, IEnumerable<CarbonCopyToken> children)
{
switch (parent)
{
case JProperty _:
return new JProperty(((JProperty)parent).Name, children.First().CarbonCopy);
case JArray _:
var newParentArray = new JArray();
foreach (var child in children)
{
newParentArray.Add(child.CarbonCopy);
}
return newParentArray;
default: // JObject, or any other type we don't recognise
var newParentObject = new JObject();
foreach (var child in children)
{
newParentObject.Add(child.CarbonCopy);
}
return newParentObject;
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,它使用了几个新类:CarbonCopyToken允许我们在逐级处理树时跟踪节点及其副本,并ObjectReferenceEqualityComparer<T>防止使用该Distinct方法出现重复(再次感谢 @dbc 指出这一点):
public class CarbonCopyToken
{
public JToken Original { get; set; }
public JToken CarbonCopy { get; set; }
}
/// <summary>
/// A generic object comparerer that would only use object's reference,
/// ignoring any <see cref="IEquatable{T}"/> or <see cref="object.Equals(object)"/> overrides.
/// </summary>
public class ObjectReferenceEqualityComparer<T> : IEqualityComparer<T> where T : class
{
// Adapted from this answer /sf/answers/132316131/
// to /sf/ask/132304091/
// By /sf/users/12409281/
private static readonly IEqualityComparer<T> _defaultComparer;
static ObjectReferenceEqualityComparer() { _defaultComparer = new ObjectReferenceEqualityComparer<T>(); }
public static IEqualityComparer<T> Default { get { return _defaultComparer; } }
#region IEqualityComparer<T> Members
public bool Equals(T x, T y)
{
return ReferenceEquals(x, y);
}
public int GetHashCode(T obj)
{
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
#endregion
}
Run Code Online (Sandbox Code Playgroud)
用法示例:
List<string> filters = new {
"$..Test1",
"$.Path.To.[*].Some.Nodes",
"$.Other.*.Nodes"
}
var result = FilterByJSONPath(inputDocument, filters);
Run Code Online (Sandbox Code Playgroud)
DotNetFiddle 显示了之前的测试以及一个额外的测试: https: //dotnetfiddle.net/ekABRI
| 归档时间: |
|
| 查看次数: |
137 次 |
| 最近记录: |