1

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"
}
Kais
  • 99
  • 12
  • 1
    Is there a reason why you can't use `[JsonExtensionData]` as you already are and have a key "properties.key1"? – ProgrammingLlama Feb 25 '22 at 07:04
  • How can I do it? – Kais Feb 25 '22 at 07:06
  • `Properties["properties.key1"] = "value1";` – ProgrammingLlama Feb 25 '22 at 07:06
  • No, I want to do it generically – Kais Feb 25 '22 at 07:10
  • @PeterCsala I would like to get this output `{ "id" : 1, "properties.key1": "value1", "properties.key2": "value2" } ` – Kais Feb 25 '22 at 12:49
  • Could you clarify your problem here? Is the problem that `Properties` is a `Dictionary` but `[JsonExtensionData]` only works for `Dictionary` or `Dictionary`? Failing demo here: https://dotnetfiddle.net/yCg8wI. Or is your problem that you want to do `Properties["key1"] = "value1";` and have the serializer automatically prepend the .NET property name `properties.` to the JSON property name? Or is it something else? – dbc Feb 25 '22 at 16:07
  • If your problem is that you need to use `Dictionary` not `Dictionary` then you can use `TypedExtensionDataConverter` and `[JsonTypedExtensionData]` from [this answer](https://stackoverflow.com/a/40094403/3744182) to [How to deserialize a child object with dynamic (numeric) key names?](https://stackoverflow.com/q/40088941/3744182), see https://dotnetfiddle.net/TWllwD. Does that answer your question? – dbc Feb 25 '22 at 16:16

2 Answers2

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.

Stephan Schlecht
  • 26,556
  • 1
  • 33
  • 47
  • Your result is the same as you would use the JsonExtensionData attribute. In the question the desired output is slightly different. Key1 and key2 are prefix with the containing dictionary name. – Peter Csala Feb 25 '22 at 22:12
  • 1
    @PeterCsala You are correct, I overlooked the composition of the desired property names. I have fixed the code and updated my answer accordingly. Thank you for pointing that out. As for JsonExtensionData, the original poster mentioned in the comments to his question that he wanted to solve the problem generically. – Stephan Schlecht Feb 25 '22 at 23:02
0

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() } }));
dbc
  • 104,963
  • 20
  • 228
  • 340
Peter Csala
  • 17,736
  • 16
  • 35
  • 75