'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();
}
}
WriteStartObjectemits{WritePropertyName(nameof(value.ID).ToLowerInvariant())emits"id":WriteValue(value.ID)emits1writer.WritePropertyName($"{prefix}.{property.Key}")emits"properties.key1":writer.WriteValue(property.Value)emits"value1"WriteEndObjectemits}
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 |
