Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

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).

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
295 views
Welcome To Ask or Share your Answers For Others

1 Answer

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...