Home > Mobile >  Clone a JsonNode and attach it to another one in .NET 6
Clone a JsonNode and attach it to another one in .NET 6

Time:03-24

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:

Demo fiddles:

  • Related