Reputation: 845
I am trying to serialize an object to JSON using newtonsoft.json. The only thing is that I cannot append the json type to the field name. Consider this example:
var item = new {
value = "value",
data = new []{"str", "str"},
b = true
};
I want to convert that to
{
"value.string" : "value",
"data.array" : ["str", "str"],
"b.bool" : true
}
or something similar. The idea is to append the json type (not the c# type) to the json field. The reason I don't want to append the C# type is because it could be complex (sometimes the type is anonymous, sometimes it is IEnumerable, etc.)
I have seen many solutions that can convert to C# type such as implementing a IContractResolver. Unfortunately that doesn't work for this case.
I also do not know the type that I will convert before hand.
The closest I could get to is
public JObject Convert(JObject data)
{
var queue = new Queue<JToken>();
foreach (var child in data.Children())
{
queue.Enqueue(child);
}
while (queue.Count > 0)
{
var token = queue.Dequeue();
if (token is JProperty p)
{
if (p.Value.Type != JTokenType.Object)
{
token.Replace(new JProperty(
$"{p.Name}.{p.Value.Type}",
p.Value
));
}
}
foreach (var child in token.Children())
{
queue.Enqueue(child);
}
}
return data;
}
But it does not work for nested objects like
var result = convertor.Convert(JObject.FromObject(new { nested = new { item = "str"}}));
For some reason, Replace does not work for the nested objects. Not sure if it is a bug or not.
Upvotes: 3
Views: 149
Reputation: 117105
Your main problem is that, when you add a child JToken
to a parent, and the child already has a parent, the child is cloned and the clone is added to the parent -- in this case your new JProperty
. Then when you replace the original property with the new property, the cloned value hierarchy replaces the original value hierarchy in the overall JToken
tree. And finally, when you do
foreach (var child in token.Children())
{
queue.Enqueue(child);
}
You end up looping through the original children that have already been cloned and replaced. While this doesn't matter when the property value is a primitive, it causes the problem you are seeing if the value is an array or other container.
(A secondary, potential issue is that you don't handle the possibility of the root container being an array.)
The fix is to prevent the wholesale cloning of property values by removing the property value from the old property before adding it to the new property, then later looping through the new property's children:
public static class JsonExtensions
{
public static TJToken Convert<TJToken>(this TJToken data) where TJToken : JToken
{
var queue = new Queue<JToken>();
foreach (var child in data.Children())
{
queue.Enqueue(child);
}
while (queue.Count > 0)
{
var token = queue.Dequeue();
if (token is JProperty)
{
var p = (JProperty)token;
if (p.Value.Type != JTokenType.Object)
{
var value = p.Value;
// Remove the value from its parent before adding it to a new parent,
// to prevent cloning.
p.Value = null;
var replacement = new JProperty(
string.Format("{0}.{1}", p.Name, value.Type),
value
);
token.Replace(replacement);
token = replacement;
}
}
foreach (var child in token.Children())
{
queue.Enqueue(child);
}
}
return data;
}
}
Working .Net fiddle.
Why does Json.NET clone the value when adding it to the new JProperty
? This happens because there is a bi-directional reference between parents and children in the JToken
hierarchy:
JToken.Children()
iterates through all child tokens of a given token;
JToken.Parent
gets the parent of a given token.
Thus a JToken
cannot have two parents -- i.e., it cannot exist in two locations in a JToken
hierarchy simultaneously. So when you add the property value to a new JProperty
, what should happen to the previous parent? Possibilities include:
The previous parent is unmodified and a clone of the child is added to the new parent.
The previous parent is modified by replacing the child with a clone of its child.
The previous parent is modified by replacing the child with a null JValue
.
As it turns out, Json.NET takes option #1, resulting in your bug.
Upvotes: 3