3

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?

  • Is it valid to have JSON with multiple keys with the same value? – mjwills Feb 16 '21 at 04:08
  • Try [ToDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.todictionary) – JL0PD Feb 16 '21 at 04:09
  • 1
    See if this helps: https://stackoverflow.com/questions/30353840/c-sharp-custom-dictionarystring-string-that-accepts-duplicate-keys-for-seriali – Yash Gupta Feb 16 '21 at 04:10
  • @Jl0PD How will that work if there are duplicate keys? – mjwills Feb 16 '21 at 04:10
  • 2
    @mjwills It will throw exception. Duplicating keys are not permitted in json, so you have to handle it. But it's possible to use DisctinctBy, which is not included into default LINQ – JL0PD Feb 16 '21 at 04:13
  • 3
    @JL0PD You may want to read what the OP wants. Since the OP _specifically wants_ duplicate keys. `ToDictionary` and `DistinctBy` won't help. – mjwills Feb 16 '21 at 04:50

1 Answers1

4

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"
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • 1
    Thanks! I hate having to duplicate the fields but I can't break old systems right now. This is exactly what I'll need to do! – user15124099 Feb 16 '21 at 18:52