How to copy a JSON value using .NET 6 JsonNode in System.Text.Json.Nodes namespace?
Asked Answered
H

2

7

I have a simple JSON I would like to rename (probably by copying then removing) a key. .NET Fiddle:

        const string input = @"{ ""foo"": [{""a"": 1}, {""b"": null}] }";
        var node = JsonNode.Parse(input);
        Console.WriteLine(node); // Output: the JSON
        Console.WriteLine(node["foo"][0]["a"]); // Output: 1
        
        // How to copy/rename?
        node["bar"] = node["foo"];
        Console.WriteLine(node["bar"][0]["a"]); // Should be 1
{
  "foo": [
    {
      "a": 1
    },
    {
      "b": null
    }
  ]
}

How do I copy/rename it? I tried using node["foo"].AsValue() but it doesn't work as well. The error is

The node must be of type 'JsonValue'


I just found a workaround by converting to JSON string and parsing it again but I hope there is a better way to do it.

node["bar"] = JsonNode.Parse(node["foo"].ToJsonString());
Hayner answered 17/2, 2022 at 2:24 Comment(1)
This question is similar to: How to duplicate a JsonNode / JsonObject / JsonArray?. If you believe it’s different, please edit the question, make it clear how it’s different and/or how the answers on that question are not helpful for your problem.Verduzco
J
13

So what you have is a JSON object, so JsonNode.Parse will really return a JsonObject, so first we need to cast it:

var jsonObject = JsonNode.Parse(input)!.AsObject(); // null check omitted for brevity

Now let's do the rename. A rename is really a removal of the object with the old property followed by readding it with the new name. So:

var node = jsonObject["foo"]; // take a reference to foo so we don't lose it
jsonObject.Remove("foo"); // detach from parent
jsonObject.Add("bar", node); // attach with new name

The reason why you need to first remove it and then add it back, is that JsonNode objects have parents, and you're not allowed to add a node to an object or array if it already has a parent. So you first need to detach it from its parent with Remove, and then you can add it back under the new name with Add.

Now, this modifies the original JSON object. If you want to instead keep the original intact you'll have to clone it first. This is a problem, because there is no way to clone a JsonNode. You could do this:

var clone = new JsonObject(jsonObject);

And it will compile, but then at runtime you'll hit an exception because this just adds the nodes from one object to the next and as we've already established you can't add an object that already has a parent to another one. You would have to first clone each child node and add those instead.

As far as I'm aware, the JsonNodes namespace has no way to do a deep clone of JSON nodes. It seems like an oversight. You could either do it by hand by recursively enumerating the nodes in the document and creating them, or just parse the document twice.

Jobye answered 17/2, 2022 at 4:9 Comment(2)
This is the correct answer for my question. Thank you for very clear explanation. However it wouldn't work if I truly want a copy (i.e. also have bar without removing foo). I intentionally kept my question a bit vague in case someone comes here for copying values as well. Would be nice if you can add that bit too. Thanks.Hayner
I added a bit about deep cloning (tl;dr it doesn't work, feel free to file an issue)Jobye
B
1

Your question update adds "bar" to an existing node object. if you want to create a new object that is a copy of an existing node object, you can make it 2 main ways

  1. The simpliest and most efficient way is to change json string
    string newInput = input.Replace("\"foo\":","\"bar\":");
    var newNode=JsonNode.Parse(newInput);
  1. Or create new json object
var newNode = new JsonObject( new[] { KeyValuePair.Create<string,JsonNode>("bar", 
               JsonNode.Parse(node["foo"].ToJsonString()) )});

result

{
  "bar": [
    {
      "a": 1
    },
    {
      "b": null
    }
  ]
}
Bluetongue answered 17/2, 2022 at 3:19 Comment(1)
Wait, why clone the input string? Strings are already immutable, string.Clone() just returns the same string (it's literally return this), but casted as an object (which forces you to use ToString(). This entire thing can be rewritten as input.Replace("\"foo\":","\"bar\":"); Anyway, it's not necessarily "more efficient" to copy the string, do string replace (which doesn't work well with more complicated inputs), and then parse it again (it's also more brittle).Jobye

© 2022 - 2024 — McMap. All rights reserved.