Husain
Husain

Reputation: 845

How can I implement custom serialisation of json appending the types to the property names?

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

Answers (1)

dbc
dbc

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:

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:

  1. The previous parent is unmodified and a clone of the child is added to the new parent.

  2. The previous parent is modified by replacing the child with a clone of its child.

  3. 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

Related Questions