5

I have the following type:

public class Product : Dictionary<string, object>
{
    [JsonInclude]
    public string ProductId { get; set; }

    public Product(string productId) : base()
    {
        ProductId = productId;
    }
}

When serialising using System.Text.Json it does not include the properties (ie ProductId). Adding or removing the [JsonInclude] does not seem to make any effect.

Test case:

[Fact]
public void SimpleTest()
{
    var p = new Product("ABC123");
    p["foo"] = "bar";
    var json = JsonSerializer.Serialize(p);
    Assert.Contains("productId", json, StringComparison.OrdinalIgnoreCase);
}

And output received:

{"foo":"bar"}

How do I make it include my custom properties on my type during serialisation? (note: don't care about deserialisation).

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Develorem
  • 143
  • 10
  • 1
    How would you expect this to output? – David L Sep 16 '21 at 03:01
  • 1
    Yes, this is correct. `System.Text.Json` only serializes the dictionary keys and values not the c# properties, as 1) there might be a key with the same name as a property, and 2) You probably don't want the "standard" properties like `Count` and `IsReadOnly` to be serialized. I can't find anywhere in the MSFT docs where this is stated, however Newtonsoft is [documented to behave this way](https://www.newtonsoft.com/json/help/html/SerializationGuide.htm#ComplexTypes) as is `DataContractJsonSerializer` and `JavaScriptSerializer`. `System.Text.Json` seems to have followed precedent. – dbc Sep 16 '21 at 03:07
  • 1
    You might consider a different data model where `Product` doesn't inherit from `Dictionary` but instead has a `[System.Text.Json.Serialization.JsonExtensionData] public Dictionary Properties { get; set; }` property. The [`[JsonExtensionData]`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonextensiondataattribute?view=net-5.0) attribute causes the dictionary properties to be included as part of the parent object when serializing. – dbc Sep 16 '21 at 03:10
  • Other than that you will need to write a custom `JsonConverter`. (According to the [docs for `JsonInclude`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonincludeattribute?view=net-5.0) *When applied to a property, indicates that non-public getters and setters can be used for serialization and deserialization.* So it isn't relevant here as `ProductId` already has public getters and setters.) – dbc Sep 16 '21 at 03:14
  • 1
    @dbc Actually the JsonExtensionData attribute might do the trick, I'll check it out, thanks. Submit it as an answer and I'll accept – Develorem Sep 16 '21 at 03:17
  • @dbc This worked perfectly, thanks. – Develorem Sep 29 '21 at 22:02

1 Answers1

3

System.Text.Json does not serialize dictionary properties. I can't find anywhere in the MSFT docs where this is stated, but System.Text.Json only serializes the dictionary keys and values. This can be confirmed from the reference source for DictionaryOfTKeyTValueConverter<TCollection, TKey, TValue>, which is the converter used for your type.

This is likely because:

  1. There might be a key with the same name as a property.
  2. App developers almost certainly don't want the "standard" dictionary properties like Count and IsReadOnly to be serialized.
  3. Earlier serializers behave the same way and System.Text.Json is following precedent. Newtonsoft is documented to only serialize dictionary keys and values. DataContractJsonSerializer (with UseSimpleDictionaryFormat = true) and JavaScriptSerializer do also.

As an alternative, you might consider a different data model where Product doesn't inherit from Dictionary but instead has a [JsonExtensionData] public Dictionary<string, object> Properties { get; set; } property:

public class Product 
{
    public string ProductId { get; set; }

    [System.Text.Json.Serialization.JsonExtensionData]
    public Dictionary<string, object> Properties { get; set; } = new ();

    public Product(string productId) : base()
    {
        ProductId = productId;
    }
}

The [JsonExtensionData] attribute causes the dictionary properties to be included as part of the parent object when serializing and deserializing.

Notes:

  • If the suggested alternative is not acceptable, you will need to write a custom JsonConverter to manually serialize the .NET properties and keys and values of your Product dictionary.

  • Adding [JsonInclude] to ProductId will not force it to get serialized. According to the docs for JsonInclude, When applied to a property, [it] indicates that non-public getters and setters can be used for serialization and deserialization. So it isn't relevant here as ProductId already has public getters and setters.

  • Newtonsoft also has a JsonExtensionData attribute (which does the same thing). If you are using both serializers, be careful to use the correct attribute.

  • In your question you state don't care about deserialisation but deserialization does work correctly with JsonExtensionData.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340