1

I have a DTO class like this:

public class MyDto
{
    public KeyValuePair<string, string> Identifier { get; set; }

    public double Value1 { get; set; }

    public string Value2 { get; set; }
}

And two instances could look like this:

var results = new List<MyDto> 
{
    new MyDto
    {
        Identifier = new KeyValuePair<string, string>("Car", "Ford"),
        Value1 = 13,
        Value2 = "A"
    },
    new MyDto 
    {
        Identifier = new KeyValuePair<string, string>("Train", "Bombardier"),
        Value1 = 14,
        Value2 = "B"
    },
};

When serializing the results within ASP.NET Web API (which uses Json.NET), the output looks like this:

[
  {
    "Identifier": {
      "Key": "Car",
      "Value": "Ford"
    },
    "Value1": 13,
    "Value2": "A"
  },
  {
    "Identifier": {
      "Key": "Train",
      "Value": "Bombardier"
    },
    "Value1": 14,
    "Value2": "B"
  }
]

But I'd like to have it that way:

[
  {
    "Car": "Ford",
    "Value1": 13,
    "Value2": "A"
  },
  {
    "Train": "Bombardier",
    "Value1": 14,
    "Value2": "B"
  }
]

How can I achieve this? Do I have to write a custom JsonConverter and do everything by hand? Since the DTO class is located in a separate assembly not having access to Json.NET, the usage of specific Json.NET attributes is not possible.

I'm not bound to using KeyValuePair<string, string>. I only need a data structure that allows me to have a flexible attribute name.

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
mu88
  • 4,156
  • 1
  • 23
  • 47
  • The `System.Text.Json` implementation of JSON in .NET Core 3+ currently has a lot of limitations, one being its inability to properly serialize `KeyValuePair` properties. Currently it's planned to be resolved in .NET 5.0 (to be released in November 2020) The ongoing issue is being tracked here: https://github.com/dotnet/runtime/issues/30524 – silkfire May 14 '20 at 13:50
  • @silkfire _When serializing these two instances using Json.NET_ – Pavel Anikhouski May 14 '20 at 13:51
  • I'm using .NET Framework, not .NET Core – mu88 May 14 '20 at 13:51
  • _Do I have to write a custom JsonConverter_ - they really aren't that complicated to do. But the easy way is to just use a single entry dictionary like Marc suggests in his answer. – Matt Burland May 14 '20 at 13:56
  • Presumably you are actually serializing a `List` even though the outer brackets aren't shown, correct? If so, just switch to a `List>` and add a single entry to each dictionary. `MyDto` is no longer necessary. That's what Marc is suggesting. – dbc May 14 '20 at 15:13
  • @dbc You're right, I'm working with an `IEnumerable`. But `MyDto` contains more than the one shown property, so I need this type. – mu88 May 14 '20 at 15:16
  • Are you saying that you need a fixed set of properties + exactly one variably named property in each object? If so that completely changes your requirement. Please [edit] your question and clarify. – dbc May 14 '20 at 15:18
  • @dbc I hope it's clearer now – mu88 May 14 '20 at 15:35
  • In that case make `Identifier` be a `Dictionary` and add `[JsonExtensionData]` as shown in [How to serialize a Dictionary as part of its parent object using Json.Net](https://stackoverflow.com/a/23786127/3744182). in fact I think this is now a duplicate. – dbc May 14 '20 at 15:51
  • I don't have access to the Json.NET library from the assembly where `MyDto` is located, so I cannot annotate the `Identifier` with `JsonExtensionData` – mu88 May 14 '20 at 15:53
  • 1
    You can't add Json.NET attributes but can you add your own custom attributes? – dbc May 14 '20 at 15:57

3 Answers3

2

Edit: this answer applies to the question as originally asked; it has subsequently been edited, and may now be less useful


You may need to use a dictionary that has a single element, i.e. Dictionary<string,string>, which does serialize in the way you want; for example:

var obj = new Dictionary<string, string> { { "Car", "Ford" } };
var json = JsonConvert.SerializeObject(obj);
System.Console.WriteLine(json);
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • I've tried your code, but it serializes to `"Identifier": { "Car": "Ford" }`, but not `{ "Car": "Ford" }` – mu88 May 14 '20 at 13:59
  • That's cause `Identifier` is the name of your property. As per the *default* behaviour, this is correct. – silkfire May 14 '20 at 14:02
  • That may be true, but it's not what I need. – mu88 May 14 '20 at 14:03
  • Then you'd need a custom converter as you suggested yourself :) – silkfire May 14 '20 at 14:21
  • 2
    @mu88 you'll notice I didn't serialize an object *with* an identifier that was a dictionary; I serialized *just the dictionary* – Marc Gravell May 14 '20 at 15:02
  • This correctly answers the question as originally written, but the question has been updated to add requirements for fixed & variable property names in the same object. – dbc May 14 '20 at 20:34
  • @dbc thanks for the heads-up; I've edited to clarify this, but ultimately: I can only answer the question as written at the time I post – Marc Gravell May 15 '20 at 05:51
2

Instead of a KeyValuePair<>, you can easily serialize a dictionary as part of a parent object by applying [JsonExtensionData] like so:

public class MyDto
{
    [JsonExtensionData]
    public Dictionary<string, object> Identifier { get; set; }

However, you have stated that the usage of specific Json.NET attributes is not possible. But since you can generally modify your DTO you could mark it with a custom extension data attribute and then handle that attribute in either a custom generic JsonConverter or a custom contract resolver.

Firstly, for an example of using a custom extension data attribute with a custom JsonConverter, see JsonTypedExtensionData from this answer to How to deserialize a child object with dynamic (numeric) key names?

Secondly, if you prefer not to use a converter, to handle a custom extension data attribute with a custom contract resolver, first define the following contract resolver:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class MyJsonExtensionDataAttribute : Attribute
{
}

public class MyContractResolver : DefaultContractResolver
{
    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);
        if (contract.ExtensionDataGetter == null && contract.ExtensionDataSetter == null)
        {
            var dictionaryProperty = contract.Properties
                .Where(p => typeof(IDictionary<string, object>).IsAssignableFrom(p.PropertyType) && p.Readable && p.Writable)
                .Where(p => p.AttributeProvider.GetAttributes(typeof(MyJsonExtensionDataAttribute), false).Any())
                .SingleOrDefault();
            if (dictionaryProperty != null)
            {
                dictionaryProperty.Ignored = true;
                contract.ExtensionDataGetter = o => 
                    ((IDictionary<string, object>)dictionaryProperty.ValueProvider.GetValue(o)).Select(p => new KeyValuePair<object, object>(p.Key, p.Value));
                contract.ExtensionDataSetter = (o, key, value) =>
                    {
                        var dictionary = (IDictionary<string, object>)dictionaryProperty.ValueProvider.GetValue(o);
                        if (dictionary == null)
                        {
                            dictionary = (IDictionary<string, object>)this.ResolveContract(dictionaryProperty.PropertyType).DefaultCreator();
                            dictionaryProperty.ValueProvider.SetValue(o, dictionary);
                        }
                        dictionary.Add(key, value);
                    };
                }
                contract.ExtensionDataValueType = typeof(object);
                // TODO set contract.ExtensionDataNameResolver
        }
        return contract;
    }
}

Then modify your DTO as follows:

public class MyDto
{
    [MyJsonExtensionData]
    public Dictionary<string, object> Identifier { get; set; }

    public double Value1 { get; set; }

    public string Value2 { get; set; }
}

And serialize as follows, caching a static instance of your resolver for performance:

static IContractResolver resolver = new MyContractResolver();

// And later

    var settings = new JsonSerializerSettings
    {
        ContractResolver = resolver,
    };

    var json = JsonConvert.SerializeObject(results, Formatting.Indented, settings);

Notes:

  • The MyJsonExtensionData property's type must be assignable to type IDictionary<string, object> and have a public, parameterless constructor.

  • Naming strategies for extension data property names are not implemented.

  • Json.NET serializes extension data attributes at the end of each object whereas your question shows the custom attributes at the beginning. Since a JSON object is defined to be an unordered set of name/value pairs by the standard I think this should not matter. But if you require the custom properties at the beginning of your object, you may need to use a custom converter rather than a custom contract resolver.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
1

Since your MyDto class is in a separate assembly for which you have limitations in the kinds of changes you can make, then yes, I think your best bet is to create a custom converter for the class. Something like this should work:

public class MyDtoConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(MyDto);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        MyDto dto = (MyDto)value;
        writer.WriteStartObject();
        writer.WritePropertyName(dto.Identifier.Key);
        writer.WriteValue(dto.Identifier.Value);
        var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(typeof(MyDto));
        foreach (JsonProperty prop in contract.Properties.Where(p => p.PropertyName != nameof(MyDto.Identifier)))
        {
            writer.WritePropertyName(prop.PropertyName);
            writer.WriteValue(prop.ValueProvider.GetValue(dto));
        }
        writer.WriteEndObject();
    }

    public override bool CanRead => false;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

To use it you will need to add the converter to your Web API configuration:

config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new MyDtoConverter());

Here is a working demo of the converter in a console app: https://dotnetfiddle.net/DksgMZ

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300