I'm using System.Text.Json.Nodes
in .NET 6.0 and what I'm trying to do is simple: Copy a JsonNode from one and attach the node to another JsonNode.
The following is my code.
public static string concQuest(string input, string allQuest, string questId) {
JsonNode inputNode = JsonNode.Parse(input)!;
JsonNode allQuestNode = JsonNode.Parse(allQuest)!;
JsonNode quest = allQuestNode.AsArray().First(quest =>
quest!["id"]!.GetValue<string>() == questId) ?? throw new KeyNotFoundException("No matching questId found.");
inputNode["quest"] = quest; // Exception occured
return inputNode.ToJsonString(options);
}
But when I try to run it, I got a System.InvalidOperationException
said "The node already has a parent."
I've tried edit
inputNode["quest"] = quest;
to
inputNode["quest"] = quest.Root; // quest.Root is also a JsonNode
Then the code runs well but it returns all nodes instead of the one I specified which is not the result I want. Also since the code works fine, I think it is feasible to set a JsonNode to another one directly.
According to the exception message, it seems if I want to add a JsonNode to another one, I must unattach it from its parent first, but how can I do this?
Note that my JSON file is quite big (more than 6MB), so I want to ensure there are no performance issues with my solution.
CodePudding user response:
Easiest option would be to convert json node into string and parse it again (though possibly not the most performant one):
var destination = @"{}";
var source = "[{\"id\": 1, \"name\":\"some quest\"},{}]";
var sourceJson = JsonNode.Parse(source);
var destinationJson = JsonNode.Parse(destination);
var quest = sourceJson.AsArray().First();
destinationJson["quest"] = JsonNode.Parse(quest.ToJsonString());
Console.WriteLine(destinationJson.ToJsonString(new() { WriteIndented = true }));
Will print:
{
"quest": {
"id": 1,
"name": "some quest"
}
}
CodePudding user response:
As JsonNode
has no Clone()
method as of .NET 6, the easiest way to copy it is probably to invoke the serializer's JsonSerializer.Deserialize<TValue>(JsonNode, JsonSerializerOptions)
extension method to deserialize your node directly into another node like so:
public static partial class JsonExtensions
{
public static string concQuest(string input, string allQuest, string questId)
{
var inputObject = JsonNode.Parse(input).ThrowOnNull().AsObject();
var allQuestArray = JsonNode.Parse(allQuest).ThrowOnNull().AsArray();
concQuest(inputObject, allQuestArray, questId);
return inputObject.ToJsonString();
}
public static JsonNode? concQuest(JsonObject inputObject, JsonArray allQuestArray, string questId)
{
// Enumerable.First() will throw an InvalidOperationException if no element is found satisfying the predicate.
var node = allQuestArray.First(quest => quest!["id"]!.GetValue<string>() == questId);
return inputObject["quest"] = node.CopyNode();
}
public static TNode? CopyNode<TNode>(this TNode? node) where TNode : JsonNode => node?.Deserialize<TNode>();
public static TNode ThrowOnNull<TNode>(this TNode? value) where TNode : JsonNode => value ?? throw new JsonException("Null JSON value");
}
Alternatively, if you aren't going to keep your array of quests around, you could just move the node from the array to the target like so:
public static partial class JsonExtensions
{
public static string concQuest(string input, string allQuest, string questId)
{
var inputObject = JsonNode.Parse(input).ThrowOnNull().AsObject();
var allQuestArray = JsonNode.Parse(allQuest).ThrowOnNull().AsArray();
concQuest(inputObject, allQuestArray, questId);
return inputObject.ToJsonString();
}
public static JsonNode? concQuest(JsonObject inputObject, JsonArray allQuestArray, string questId)
{
// Enumerable.First() will throw an InvalidOperationException if no element is found satisfying the predicate.
var (_, index) = allQuestArray.Select((quest, index) => (quest, index)).First(p => p.quest!["id"]!.GetValue<string>() == questId);
return MoveNode(allQuestArray, index, inputObject, "quest");
}
public static JsonNode? MoveNode(JsonArray array, int id, JsonObject newParent, string name)
{
var node = array[id];
array.RemoveAt(id);
return newParent[name] = node;
}
public static TNode ThrowOnNull<TNode>(this TNode? value) where TNode : JsonNode => value ?? throw new JsonException("Null JSON value");
}
Also, you wrote
since my json file is quite big (more than 6MB), I was worried there might be some performance issues.
In that case I would avoid loading the JSON files into the input
and allQuest
strings because strings larger than 85,000 bytes go on the large object heap which can cause subsequent performance degradation. Instead, deserialize directly from the relevant files into JsonNode
arrays and objects like so:
var questId = "2"; // Or whatever
JsonArray allQuest;
using (var stream = new FileStream(allQuestFileName, new FileStreamOptions { Mode = FileMode.Open, Access = FileAccess.Read }))
allQuest = JsonNode.Parse(stream).ThrowOnNull().AsArray();
JsonObject input;
using (var stream = new FileStream(inputFileName, new FileStreamOptions { Mode = FileMode.Open, Access = FileAccess.Read }))
input = JsonNode.Parse(stream).ThrowOnNull().AsObject();
JsonExtensions.concQuest(input, allQuest, questId);
using (var stream = new FileStream(inputFileName, new FileStreamOptions { Mode = FileMode.Create, Access = FileAccess.Write }))
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
input.WriteTo(writer);
Or, if your app is asynchronous, you can do:
JsonArray allQuest;
await using (var stream = new FileStream(allQuestFileName, new FileStreamOptions { Mode = FileMode.Open, Access = FileAccess.Read, Options = FileOptions.Asynchronous }))
allQuest = (await JsonSerializer.DeserializeAsync<JsonArray>(stream)).ThrowOnNull();
JsonObject input;
await using (var stream = new FileStream(inputFileName, new FileStreamOptions { Mode = FileMode.Open, Access = FileAccess.Read, Options = FileOptions.Asynchronous }))
input = (await JsonSerializer.DeserializeAsync<JsonObject>(stream)).ThrowOnNull();
JsonExtensions.concQuest(input, allQuest, questId);
await using (var stream = new FileStream(inputFileName, new FileStreamOptions { Mode = FileMode.Create, Access = FileAccess.Write, Options = FileOptions.Asynchronous }))
await JsonSerializer.SerializeAsync(stream, input, new JsonSerializerOptions { WriteIndented = true });
Notes:
- Microsoft's documentation explicitly recommends against serializing from and to strings instead of UTF-8 byte sequences for performance reasons which is another reason not to load your large JSON files into temporary string buffers.
Demo fiddles:
For copying the node, see https://dotnetfiddle.net/cwKDen.
For moving the node, see https://dotnetfiddle.net/cI8DuB.
For async reading and writing, see https://dotnetfiddle.net/VjKstQ