user15124099
user15124099

Reputation: 29

How do you make a tuple or keyvalue pair json serialize like a dictionary in C#

I've got an IEnumerable of either Tuple<string,string> or KeyValuePair<string,string> (I'm flattening a list of property values to get them)

I want it to json serialize like a dictionary where the key is the property but I've got duplicate keys after the flattening so I can't use dictionary.

so I want

{
"test": "value1",
"test": "value2",
"test": "value3",
"test2": "somevalue"
}

Here's how I'm coming up with the flattened list

Values.SelectMany(a => a.Value.Select(b => new KeyValuePair<string, string>(a.Key, b)));

What can I do to make it easy and just call JsonSerializer.Serialize on the output to get what I want?

Upvotes: 3

Views: 1714

Answers (1)

Peter Csala
Peter Csala

Reputation: 22829

One way to solve this problem is via the usage of ILookup<,> and a custom JsonConverter.

  • You can think of the ILookup<T1, T2> as a Dictionary<T1, IEnumerable<T2>>
  • So, it is a Bag data structure.
var dataSource = new List<KeyValuePair<string, string>>
{
    new KeyValuePair<string, string>("test", "value1"),
    new KeyValuePair<string, string>("test", "value2"),
    new KeyValuePair<string, string>("test", "value3"),
    new KeyValuePair<string, string>("test2", "somevalue"),
};
var toBeSerializedData = dataSource.ToLookup(pair => pair.Key, pair => pair.Value);
var serializedData =JsonConvert.SerializeObject(toBeSerializedData);

This will generate the following json:

[
  [
    "value1",
    "value2",
    "value3"
  ],
  [
    "somevalue"
  ]
]
  • As you see the values are grouped by the Keys.
  • But the keys are omitted and the values are in arrays.

In order to overcome of these we can define a custom JsonConverter:

public class LookupSerializer : JsonConverter
{
    public override bool CanConvert(Type objectType) => objectType.GetInterfaces()
        .Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(ILookup<,>));

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
    {
        writer.WriteStartObject();
        foreach (object values in (IEnumerable)value)
        {
            var keyProp = values.GetType().GetProperty("Key");
            var keyValue = keyProp.GetValue(values, null);

            foreach (var val in (IEnumerable)values)
            {
                writer.WritePropertyName(keyValue.ToString());
                writer.WriteValue(val.ToString());
            }
        }

        writer.WriteEndObject();
    }

    public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 
        => throw new NotImplementedException(); 
}
  • CanConvert: restricts the usage only for ILookup.
  • WriteJson: iterates through the groups with the outer foreach and iterates through the values via the inner foreach.
  • Here we can't use JObject (and its Add method) to create the output because it would fail with an ArgumentException:

Can not add property test to Newtonsoft.Json.Linq.JObject.
Property with the same name already exists on object.

  • So, we have to use lower level JsonWriter to construct the output.

If you pass an instance of the LookupSerialzer to the SerializeObject (or register the converter application-wide):

var serializedData =JsonConvert.SerializeObject(toBeSerializedData, new LookupSerializer());

then the output will be the one as desired:

{
  "test" : "value1",
  "test" : "value2",
  "test" : "value3",
  "test2" : "somevalue"
}

Upvotes: 4

Related Questions