'How to serialize Dictionary as part of it's parent with a specific converter using Json.Net

I'm using Json.Net for Serialization. I have this class

public class Test
{
    public int ID { get; set; }

    public Dictionary<string, string> Properties { get; set; }
}

Can I serialize this object to get the following JSON ?

{
    "id" : 1,
    "properties.key1": "value1",
    "properties.key2": "value2"
}


Solution 1:[1]

I was trying to find a solution that was general and not specific to a particular class (e.g., "test"), so I assume that each dictionary<string,string> should be moved to its parent at each nesting level.

One possible way would be to use two stages:

  • to define a custom DefaultContractResolver that adds additional synthetic information about all properties to be flattened in an array with a specific name, I used ___flatten___.
  • then traversing the object tree, reading the array of properties to be flattened, replacing each of them with the key-value pairs, and finally removing the temporarily used synthetic ___flatten___ property

The FlattenedResolver that add the synthetic ___flatten___ property could look like this:

using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


class FlattenedResolver : DefaultContractResolver
{

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);

        var toFlatten = new List<JsonProperty>();
        foreach (var p in props)
        {
            if (p.PropertyType == typeof(Dictionary<string, string>))
            {
                toFlatten.Add(p);
            }
        }

        var flattenArray = toFlatten.Select(p => p.PropertyName).ToArray();
        if (flattenArray.Length > 0)
        {
            props.Insert(0, new JsonProperty
            {
                DeclaringType = type,
                PropertyType = typeof(string []),
                PropertyName = "___flatten___",
                ValueProvider = new ArrayValueProvider(flattenArray),
                Readable = true,
                Writable = false
            });
        }


        return props;
    }

    class ArrayValueProvider : IValueProvider
    {
        private readonly string[] properties;
        public ArrayValueProvider(string[] properties)
        {
            this.properties = properties;
        }
        public object GetValue(object target)
        {
            return properties;
        }

        public void SetValue(object target, object value)  { }
    }

}

After the first stage the provided example JSON would look like this:

{
  "___flatten___": [
    "Properties"
  ],
  "ID": 1,
  "Properties": {
    "key1": "value1",
    "key2": "value2"
  }
}

In the second stage the actual flattening happens. Codewise this would look like this:

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public static class JsonTransformer
{
    
    public static string FlattenedJson(object obj)
    {
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new FlattenedResolver(),
            Formatting = Formatting.Indented
        };
        var serializer = JsonSerializer.Create(settings);
        var token = JToken.FromObject(obj, serializer);
        System.Console.WriteLine(token.ToString());
        Transform(token);
        return token.ToString();
    }

    private static void Transform(JToken node)
    {
        if (node.Type == JTokenType.Object)
        {
            ResolveFlattened((JObject) node);
            foreach (JProperty child in node.Children<JProperty>())
            {
                Transform(child.Value);
            }
        }
        else if (node.Type == JTokenType.Array)
        {
            foreach (JToken child in node.Children())
            {
                Transform(child);
            }
        }
    }


    private static void ResolveFlattened(JObject node)
    {
        var toFlatten = new List<string>();

        foreach (var property in node.Children<JProperty>())
        {
            if (property.Name.StartsWith("___flatten___"))
            {
                toFlatten = property.Value.Select(v => v.ToString()).ToList();
            }
        }

        node.Remove("___flatten___");

        foreach (var propertyName in toFlatten)
        {
            var token = node[propertyName];
            node.Remove(propertyName);
            if (token == null) continue;
            foreach (var keyValueToken in token)
            {
                var property = keyValueToken as JProperty;
                string composedPropertyName = $"{propertyName}.{property.Name}";
                if (!String.IsNullOrEmpty(composedPropertyName))
                    composedPropertyName = Char.ToLower(composedPropertyName[0]) + composedPropertyName.Substring(1);

                node.Add(composedPropertyName, property.Value.ToString());
            }
        }
    }
}

If you now do a quick test e.g. with this code:

public static class Program
{
    
    static void Main()
    {
        Test test = new Test(id: 1, properties: new()
        {
            {"key1", "value1"},
            {"key2", "value2"}
        });
        string json = JsonTransformer.FlattenedJson(test);
        System.Console.WriteLine(json);
    }

}

you will get the desired result:

{
    "ID": 1,
    "properties.key1": "value1",
    "properties.key2": "value2"
}

As mentioned above, this solution is generic and not specific to a particular class to be serialized.

Solution 2:[2]

With the following custom JsonConverter<T> you can specify how do you want to serialize each and every data of Test class instance:

public class TestConverter : JsonConverter<Test>
{
    public override Test ReadJson(JsonReader reader, Type objectType, Test existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, Test value, JsonSerializer serializer)
    {
        writer.WriteStartObject();
        writer.WritePropertyName(nameof(value.ID).ToLowerInvariant());
        writer.WriteValue(value.ID);
        var prefix = nameof(value.Properties).ToLowerInvariant();
        foreach (var property in value.Properties)
        {
            writer.WritePropertyName($"{prefix}.{property.Key}");
            writer.WriteValue(property.Value);
        }
        writer.WriteEndObject();
    }
}
  • WriteStartObject emits {
  • WritePropertyName(nameof(value.ID).ToLowerInvariant()) emits "id":
  • WriteValue(value.ID) emits 1
  • writer.WritePropertyName($"{prefix}.{property.Key}") emits "properties.key1":
  • writer.WriteValue(property.Value) emits "value1"
  • WriteEndObject emits }

Usage:

var test = new Test
{
    ID = 1,
    Properties = new Dictionary<string, string>
    {
        { "key1", "value1" },
        { "key2", "value2" },
    }
};
Console.WriteLine(JsonConvert.SerializeObject(test, settings:
    new() { Converters = new[] { new TestConverter() } }));

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 dbc