1

I extended a dictionary (which is perfect data structure for translations) and added a marker telling what kind of translation will be performed.

internal class Translation : Dictionary<string, string>
{
  public string Name { get; set; }
}

However, when I serialize the object, I only get the key-value pairs in my output string. The name doesn't show. I wanted to use the stuff from the goodie bag from uncle Microsoft, i.e. System.Text.Json, so I do the following.

string output = JsonSerializer.Serialize(source);

My suspicion is that I will need to implement a custom serializer but that's way too much hustle for this simple case. My experience tells me there's a neat, smooth approach bundled in the tools (one that I'm simply not aware of).

How to do it? Alternatively, if not possible smoothly, why is it a complex matter (that I'm apparently failing to appreciate)?

I was expecting a JSON on form below.

{
  "name": "donkey",
  "key1": "value1",
  "key2": "value2",
  "key3": "value3",
}

I can resolve it by adding an item to my dictionary with key being name and value being donkey, of course. But that pragmatic solution, I prefer to save as my fall-back. At the moment I have some extra time and want to play around with the structure. Also, I can imagine that the name might become an int instead of string or maybe even a more complex structure to describe e.g. timestamp or something. That would totally break the contract of the dictionary (being string-to-string mapping).

dbc
  • 80,875
  • 15
  • 141
  • 235
Konrad Viltersten
  • 28,018
  • 52
  • 196
  • 347
  • Do you want to stick to the native `System.Text.Json` or are you up to use json.net ? – Cid Dec 27 '19 at 09:11
  • @Cid For this case, I want to try out the new goodies. [Checking the performance](https://michaelscodingspot.com/the-battle-of-c-to-json-serializers-in-net-core-3/), there's no bigger risk in switching and it offers an opportunity to broaden the competence. Since time isn't of much concern (Christmas holidays and stuff), I can afford to waste some effort without my boss lurking over my shoulder, hehe. Otherwise, I'd go with Newton due to extensive blogization around it. :) – Konrad Viltersten Dec 27 '19 at 09:45
  • This can be the same issue explained [there](https://stackoverflow.com/questions/14383736/how-to-serialize-deserialize-a-custom-collection-with-additional-properties-usin) for newtonsoft.json. You may have to go with a custom converter – Cid Dec 27 '19 at 09:58
  • However, a lazy workaround could be to change the structure of your class, instead of extending a `Dictionary`, use that as a class property – Cid Dec 27 '19 at 10:00
  • https://www.newtonsoft.com/json/help/html/SerializationGuide.htm#Dictionarys – Hans Passant Dec 27 '19 at 10:33
  • Does your sample work? Because there are a lot of GitHub issues (like [this](https://github.com/dotnet/corefx/issues/40120) or [this](https://github.com/dotnet/corefx/issues/41283)) showing that attempts to serialize a dictionary can cause `NotSupportedException` – Pavel Anikhouski Dec 27 '19 at 11:34
  • @PavelAnikhouski It works (as in no exceptions). It lacks the extra field, though, so it works unsatisfactorily. :) – Konrad Viltersten Dec 27 '19 at 20:34
  • @HansPassant Does that relate to the new JSON converter from *System.Text.Json*? I'm not sure if I understand the point of the link provided. Are you saying that it can't be done regardless, due to how a dictionary inheritance works? – Konrad Viltersten Dec 27 '19 at 20:36
  • @KonradViltersten There are a lot of unfinished things in `System.Text.Json`, per GitHub. I've got the same behavior for `List`, extended property isn't serilaized, using custom converter seems to be only one option here, at least for now. There are some examples [here](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?view=netcore-3.1#support-dictionary-with-non-string-key) – Pavel Anikhouski Dec 28 '19 at 14:49
  • @PavelAnikhouski But... but... it's Christmas... And it's supposed to be a goodie bag from Microsoft and uncle Billy G... Maybe I got too excited and went ahead of myself on this one. :( – Konrad Viltersten Dec 28 '19 at 15:15

1 Answers1

2

This seems to be the design intent -- as with Newtonsoft, JavaScriptSerializer and DataContractJsonSerializer, the dictionary keys and values are serialized, not the regular properties.

As an alternative to extending Dictionary<TKey, TValue>, you can get the JSON you want by encapsulating a dictionary in a container class and marking the dictionary with JsonExtensionDataAttribute:

internal class Translation
{
    public string Name { get; set; }

    [JsonExtensionData]
    public Dictionary<string, object> Data { get; set; } = new Dictionary<string, object>();
}

And then serialize as follows:

var translation = new Translation
{
    Name = "donkey",
    Data = 
    {
        {"key1", "value1"},
        {"key2", "value2"},
        {"key3", "value3"},
    },
};

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    // Other options as required
    WriteIndented = true,
};

var json = JsonSerializer.Serialize(translation, options);

Do note this restriction from the docs

The dictionary's TKey value must be String, and TValue must be JsonElement or Object.

(As an aside, a similar approach would work with Newtonsoft which has its own JsonExtensionDataAttribute. If you are using both libraries, be sure not to get the attributes confused.)

Demo fiddle #1 here.

If this modification to your data model is not convenient, you can introduce a custom JsonConverter<Translation> that (de)serializes a DTO like the model above, then maps the DTO from and to your final model:

internal class Translation : Dictionary<string, string>
{
    public string Name { get; set; }
}

internal class TranslationConverter : JsonConverter<Translation>
{
    internal class TranslationDTO
    {
        public string Name { get; set; }

        [JsonExtensionData]
        public Dictionary<string, object> Data { get; set; }
    }

    public override Translation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var dto = JsonSerializer.Deserialize<TranslationDTO>(ref reader, options);
        if (dto == null)
            return null;
        var translation = new Translation { Name = dto.Name };
        foreach (var p in dto.Data)
            translation.Add(p.Key, p.Value?.ToString());
        return translation;
    }

    public override void Write(Utf8JsonWriter writer, Translation value, JsonSerializerOptions options)
    {
        var dto = new TranslationDTO { Name = value.Name, Data = value.ToDictionary(p => p.Key, p => (object)p.Value) };
        JsonSerializer.Serialize(writer, dto, options);
    }
}

And then serialize as follows:

var translation = new Translation
{
    Name = "donkey",
    ["key1"] = "value2",
    ["key2"] = "value2",
    ["key3"] = "value3",
};

var options = new JsonSerializerOptions
{
    Converters = { new TranslationConverter() },
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    // Other options as required
    WriteIndented = true,
};

var json = JsonSerializer.Serialize(translation, options);

I find it simpler to (de)serialize to a DTO rather than to work directly with Utf8JsonReader and Utf8JsonWriter as edge cases and naming policies get handled automatically. Only if performance is critical will I work directly with the reader and writer.

With either approach JsonNamingPolicy.CamelCase is required to bind "name" in the JSON to Name in the model.

Demo fiddle #2 here.

dbc
  • 80,875
  • 15
  • 141
  • 235