I have an arbitrarily defined JSON document, and I want to be able to apply a JSONPath expression like a whitelist filter for properties: All selected nodes and their ancestors back to the root node remain, all other nodes are removed. If the nodes don't exist, I should end up with an empty document.
There didn't seem to be anything similar to this built into JSON.Net and I couldn't find similar examples anywhere, so I built my own. I opted to copy selected nodes into a newly built document rather than try and remove all nodes that didn't match. Given that there could be multiple matches and documents could be large, it needed to be able to handle merging the multiple selection results efficiently into a single tree/JSON document.
My attempt sort of works, but I'm getting strange results. The process involves a MergedAncestry
method which iterates over the SelectTokens
results, calls GetFullAncestry
(which recursively builds the tree to that node), then merges the results. It seems the merging of JArrays is happening at the wrong level though, as you can see under "Actual results" below.
My questions:
- Is there a better/faster/built-in way to achieve this?
- If not, what am I doing wrong?
Code:
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;
}
}
Example 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" }
]
}
]
}
See DotNetFiddle for full code and tests
"Filter" JSONPath:
$.Array3B.[*].Array3B1.[*].*
Expected results:
{
"Array3B": [
{
"Array3B1": [
{ "Item_1": "Desc_3B11" },
{ "Item_2": "Desc_3B12" },
{ "Item_3": "Desc_3B13" }
]
}
]
}
Actual results:
{
"Array3B": [
{
"Array3B1": [ { "Item_1": "Desc_3B11" } ]
},
{
"Array3B1": [ { "Item_2": "Desc_3B12" } ]
},
{
"Array3B1": [ { "Item_3": "Desc_3B13" } ]
}
]
}