6

There is a large JSON file (about a thousand lines). The task is to update existing JProperties, or add new JProperties in a specific location in the structure. The location of the new texts are based on the JToken.Path property. For example, this is the start of the JSON:

"JonSnow": {
    "Direwolf": {
        "Name": "Ghost",
        "Color": "White",
    }
}
"DanaerysTargaryen": {
    "Dragons": {
        "Dragon1": {
            "Name": "Drogon",
        }
    }
    "Hair": {
        "Color": "White"
    }
}

Now the JSON must be updated using a given list of JToken paths and the corresponding values.

The first possibility is, the JProperty corresponding to the path might already exist, in which case the value needs to be updated. I am already successfully implementing this with JToken.Replace().

The second possibility is, the JProperty does not exist yet and needs to be added. For example, I need to add "DanaerysTargaryen.Dragons.Dragon1.Color" with the value "Black".

I know I can use the JSON.Net Add() method, but to use this only the final child token of the path can be missing from the JSON. For example, I can use

JObject ObjToUpdate= JObject.Parse(jsonText);
JObject Dragon = ObjToUpdate["DanaerysTargaryen"]["Dragons"]["Dragon1"] as JObject;
Dragon.Add("Color", "Black"));

But what about if I need to add "JonSnow.Weapon.Type" with the value "Longsword"? Because "Weapon" does not exist yet as a JProperty, and it needs to be added along with "Type" : "Longsword". With each path, it is unknown how much of the path already exists in the JSON. How can this be parameterised?

// from outside source: Dictionary<string, string> PathBasedDict 
// key: Jtoken.Path (example: "JonSnow.Weapon.Type")
// value: new text to be added (example: "Longsword")

foreach(KeyValuePair entry in PathBasedDict)
{
    string path = entry.Key;
    string newText = entry.Value;

    if (ObjToUpdate.SelectToken(path) != null)  
        { ObjToUpdate.SelectToken(path).Replace(newText); }

    else AddToJson(path, newText);
}

What should AddToJson() look like? Iterating through the entire path and checking each possible JProperty to see if it exists, and then adding the rest underneath, seems very cumbersome. Is there a better way to do this? Any Json.NET tricks I am unaware of? I am not even sure how the iteration could be parameterised.

user9729207
  • 63
  • 1
  • 5
  • You could just split your path by `"."`, then `foreach` over that, adding or retrieving the `JToken` as you go... – Heretic Monkey Jun 03 '19 at 12:27
  • Heretic Monkey, thanks for the tip. But to add the JToken, I also have to add the other JProperties which it contains, that the `foreach` has not reached yet. Am I understanding this wrongly? – user9729207 Jun 03 '19 at 14:09
  • Another option is to create your data object from the JSON using an [ExpandoObject](https://learn.microsoft.com/en-us/dotnet/api/system.dynamic.expandoobject) (from System.Dynamic) and build/modify your object(s) at run-time, add or remove properties if/when required, then serialize the new JSON. See also [Walkthrough: Creating and Using Dynamic Objects](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/walkthrough-creating-and-using-dynamic-objects) – Jimi Jun 03 '19 at 15:03
  • Related or duplicate: [How to unflatten flattened json in C#](https://stackoverflow.com/q/40541842/3744182). The accepted answer there populates arrays as well as objects. – dbc Jul 23 '21 at 17:24

2 Answers2

7

Based on first approach from Heretic Monkey's answer, here is an extension method:

public static class JObjectExtensions
{
    /// <summary>
    /// Replaces value based on path. New object tokens are created for missing parts of the given path.
    /// </summary>
    /// <param name="self">Instance to update</param>
    /// <param name="path">Dot delimited path of the new value. E.g. 'foo.bar'</param>
    /// <param name="value">Value to set.</param>
    public static void ReplaceNested(this JObject self, string path, JToken value)
    {
        if (self is null)
            throw new ArgumentNullException(nameof(self));
        
        if (string.IsNullOrEmpty(path))
            throw new ArgumentException("Path cannot be null or empty", nameof(path));

        var pathParts = path.Split('.');
        JToken currentNode = self;
        
        for (int i = 0; i < pathParts.Length; i++)
        {
            var pathPart = pathParts[i];
            var isLast = i == pathParts.Length - 1;
            var partNode = currentNode.SelectToken(pathPart);
            
            if (partNode is null)
            {
                var nodeToAdd = isLast ? value : new JObject();
                ((JObject)currentNode).Add(pathPart, nodeToAdd);
                currentNode = currentNode.SelectToken(pathPart);
            }
            else
            {
                currentNode = partNode;

                if (isLast)
                    currentNode.Replace(value);
            }
        }
    }
}
yurislav
  • 1,119
  • 12
  • 17
5

There are a few ways of going about this. Here are two of them.

  1. To go along with your existing code, split the path by '.', then iterate over them. If the path is not there, create it with Add. Otherwise, if we're on the last part of the path, just add the value.

    var json = JObject.Parse(@"{""DanaerysTargaryen"":{""Dragons"":{""Dragon1"":{""Name"": ""Drogon""}},""Hair"": {""Color"": ""White""}}}");
    var toAdd = "DanaerysTargaryen.Dragons.Dragon1.Color";
    var valueToAdd = "Black";
    var pathParts = toAdd.Split('.');
    JToken node = json;
    for (int i = 0; i < pathParts.Length; i++)
    {
        var pathPart = pathParts[i];
        var partNode = node.SelectToken(pathPart);
        if (partNode == null && i < pathParts.Length - 1)
        {
            ((JObject)node).Add(pathPart, new JObject());
            partNode = node.SelectToken(pathPart);
        }
        else if (partNode == null && i == pathParts.Length - 1)
        {
            ((JObject)node).Add(pathPart, valueToAdd);
            partNode = node.SelectToken(pathPart);
        }
        node = partNode;
    }
    
    Console.WriteLine(json.ToString());
    

(Example on dotnetfiddle.net)

  1. Otherwise, you could create a separate JObject that represents the node(s) you want to add, then merge them.

     var json = JObject.Parse(@"{""DanaerysTargaryen"":{""Dragons"":{""Dragon1"":{""Name"": ""Drogon""}},""Hair"": {""Color"": ""White""}}}");
     var toMerge = @"{""DanaerysTargaryen"":{""Dragons"":{""Dragon1"":{""Color"":""Black""}}}}";
     var jsonToMerge = JObject.Parse(toMerge);
     json.Merge(jsonToMerge);
     Console.WriteLine(json.ToString());
    

(Example on dotnetfiddle.net)

Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
  • In first approach, It is dangerous to determine last path part based on string equality. Foreach should be replaced with for loop. – yurislav Aug 29 '20 at 14:53
  • "It is dangerous"... what's the danger? "Foreach should be replaced with for loop." Why? I'm fine with criticism, but I take it more seriously when backed up with facts and citations, not just assertions. – Heretic Monkey Aug 29 '20 at 15:31
  • Sorry for not being clear enough, the code do not handle path with repeating part paths, such as "foo.bar.foo.bar", it could jump to incorrect if/elseif statement due to string comparsion of part path, thats why I advised that e.g. for loop with index comaprsion should be done. – yurislav Aug 31 '20 at 07:18
  • Oh, I see, yes, this code was specific to the OP's situation, but I see how it would fail in the general case. Thanks! Edited. – Heretic Monkey Sep 02 '20 at 21:20