3

I am having 2 scinarios to show the issue.

Scenario 1

using System;
using Newtonsoft.Json.Linq;

public class Program
{
    public static void Main()
    {
        var arr = new JArray();
        arr.Add("apple");
        var obj = new JObject();
        obj["arr"] = arr;
        obj["arr"] = arr;       
        arr.Add("mango");
        
        foreach(var a in obj["arr"]){
            Console.WriteLine(a);
        }
        
                            
    }
}

Here obj["array"] should be referenceing the arr, i.e. initialized earlier. So the output should be

apple
mango

but the output was

apple

Scenario 2

using System;
using Newtonsoft.Json.Linq;

public class Program
{
    public static void Main()
    {
        var arr = new JArray();
        arr.Add("apple");
        var obj = new JObject();
        obj["arr"] = arr;
        
        var obj2 = new JObject();
        obj2["arr"] = arr;      
        
        arr.Add("mango");
        
        foreach(var a in obj2["arr"]){
            Console.WriteLine(a);
        }
    }
}

Similarly obj2["arr"] should be referencing the arr. but it is not. So the expected output is

apple
mango

but the output is

apple

I am not that proficient in csharp. Please let me know if i am missing something here.

Edit

Adding another scenario as mentioned by @Wyck in comments.

Scenario 3

using System;
using Newtonsoft.Json.Linq;

public class Program
{
    public static void Main()
    {
        var arr = new JArray();
        Console.WriteLine(arr.GetHashCode());
        arr.Add("apple");
        var obj = new JObject();
        obj["arr"] = arr;
        Console.WriteLine(obj["arr"].GetHashCode());
        obj["arr"] = arr;   
        Console.WriteLine(obj["arr"].GetHashCode());
        obj["arr"] = arr;   
        Console.WriteLine(obj["arr"].GetHashCode());
        
        arr.Add("mango");
        
        foreach(var a in obj["arr"]){
            Console.WriteLine(a);
        }
        
                            
    }
}

Repeating the assignment obj["arr"] = arr odd number of time gets back the original reference of arr but doing so even number of times doesn't.

The output of this will be

10465620
10465620
1190878
10465620
apple
mango

see the hash code is changed for even number assignment. for the odd number assignment it again became as before.

ashutosh
  • 324
  • 2
  • 8
  • What did you hope to do by writing `obj["arr"] = arr;` twice? – Wyck Mar 21 '22 at 17:06
  • @Wyck yeah, bit weird, but why it fails buzzles me, seems like a bug to me! See my comment on Krik's answer. – Ghasan غسان Mar 21 '22 at 17:07
  • 1
    Yes, this is correct. For an explanation why see [nested json objects dont update / inherit using Json.NET](https://stackoverflow.com/a/29260565/3744182) and [JArray.Remove(JToken) does not delete](https://stackoverflow.com/a/47826061/3744182). – dbc Mar 21 '22 at 17:20
  • It may be "correct" but it is certainly "surprising"! – Wyck Mar 21 '22 at 17:22
  • @Wyck - I believe LINQ-to-XML works similarly since the parent/child graph is doubly-connected there also. – dbc Mar 21 '22 at 17:22
  • @dbc - Scenario 2 is clear from your explanation. JToken can't have 2 parent. But how the behaviour shown by Wyck can be explained i.e. for odd number of times it works but not for even number of times... ! – ashutosh Mar 21 '22 at 18:54
  • @ashutosh - I would need to see an [mcve], but **maybe** Json.NET doesn't check when you are trying to replace a token with itself (i.e. identical down to reference equality) so when you set `arr` the first time it gets cloned and de-parented, then when you set it the second time it gets set without cloning (because the parent was cleared). A full example would clarify. – dbc Mar 21 '22 at 19:01
  • @dbc - i have updated my question to show that odd and even number assignment case. – ashutosh Mar 21 '22 at 19:24

2 Answers2

2

If you look at the source code for Newtonsoft.Json, you will find that when assigning an array to a property, it will create a copy of it:

public JProperty(string name, object? content)
{
    ...
    Value = IsMultiContent(content)
        ? new JArray(content)
        : CreateFromContent(content);
}

The relevent part of JObject is here.

You can easily test this in your code by getting the hash code (.GetHashCode()) of both obj2["arr"] and arr (both of type JArray) and observe that they will be different.

So in order to be able to add to the array, you need to access it via the instance of JObject once the property is assigned, or you can re-assign the array to the property whenever you add an element.

Kirk Woll
  • 76,112
  • 22
  • 180
  • 195
  • Thought so, but there is something off. If you assign it once, it works by referencing the original array (not through JObject), however, if you assign it twich (for whatever reason you are doing that), it seems to create a copy. However, if you assign something else like arr2, and then assign it again, it works! **works**: `obj["arr"] = arr; obj["arr"] = arr2; obj["arr"] = arr;` **works:** `obj["arr"] = arr;` **doesn't work!**: `obj["arr"] = arr; obj["arr"] = arr;` – Ghasan غسان Mar 21 '22 at 17:04
  • More strangely if you do `obj["arr"] = arr` an **odd** number of times it works. If you do it an **even** number of times, you get some other object. (try doing it 3, 4, 5, or 6 times!) – Wyck Mar 21 '22 at 17:21
  • 1
    @Ghasanغسان - your observation is correct. A `JToken` can have only one parent, so cloning is only required when trying to add a token that already has a parent. – dbc Mar 21 '22 at 17:21
0

This answer is totally based on @dbc's comment. This surprising behaviour is due to the way Json.net is implemented. The original answer is here.

All JToken needs to have a Parent property and it can have only one Parent.

obj["arr"] = arr;

Here before setting arr, it is verified if arr has a Parent already. if Parent is null, arr will be assigned unmodified, otherwise arr will be cloned and the cloned arr will be assigned. This the reason behind the behaviour of Scenario 1 and Scenario 2.

using System;
using Newtonsoft.Json.Linq;

public class Program
{
    public static void Main()
    {
        var arr = new JArray();
        
        arr.Add("apple");
        var obj = new JObject();
        obj["arr"] = arr;
        Console.WriteLine(arr.Parent == null ? "null" : arr.Parent.GetHashCode().ToString());
        obj["arr"] = arr;
        Console.WriteLine(arr.Parent == null ? "null" : arr.Parent.GetHashCode().ToString());
        obj["arr"] = arr;   
        Console.WriteLine(arr.Parent == null ? "null" : arr.Parent.GetHashCode().ToString());
        
        arr.Add("mango");
        
        foreach(var a in obj["arr"]){
            Console.WriteLine(a);
        }
        
                            
    }
}

The output of this code block will be

10566368
null
10566368
apple
mango

It can be seen, after even number of assignments the Parent of arr is set to null.

After the first assignment arr.Parent is set (JProperty containing the Name and its Value, which belongs JOject). During the second assignment, as arr already has a Parent, arr will be cloned and the cloned value will be set to the obj["arr"]. As a new JToken is set, Parent of the previous JToken will be set to null i.e. arr.Parent will become null.

Again on third assignment, arr.Parent is null. So it will be set like the first assignment.

for Scenario 2 arr already has a Parent, so during the 2nd assignment i.e. obj2["arr"] = arr;, arr will be cloned and set.

ashutosh
  • 324
  • 2
  • 8