Inherit attribute during serialization when implementing abstract base field

Viewed 551

I have an abstract class Base that wants every derived class to implement a property SortValue. This property should have an attribute applied for all derived instances, in this case JsonIgnore.

What's happening appears to be that the attribute is not respected for Derived. At least, the property is not ignored when serialized by System.Text.Json.

How can I achieve attribute inheritance while ensuring every subclass implements SortValue?

public abstract class Base
{
    [JsonIgnore]
    public abstract IComparable SortValue { get; }
}

public class Derived : Base
{
    public int VoteCount { get; set; }

    // Desire: inherited JsonIgnore attribute
    public override IComparable SortValue => VoteCount;
}
1 Answers

Update: this is intended behavior of System.Text.Json:

In comments Noah Stahl links to this issue

System.Text.Json.JsonSerializer.Serialize ignores JsonPropertyName on abstract properties #3979

This is known/by design. Properties cannot inherit System.Text.Json attributes. The attributes need to be placed on each (de)serializable override.

As a workaround, you could make make SortValue be a public, non-virtual surrogate that calls some protected abstract property like so:

public abstract class Base
{
    protected abstract IComparable ProtectedSortValue { get; }
    
    [JsonIgnore]
    public IComparable SortValue => ProtectedSortValue;
}

public class Derived : Base
{
    public int VoteCount { get; set; }

    protected override IComparable ProtectedSortValue => VoteCount;
}

With this change, SortValue will not be serialized in derived classes while ProtectedSortValue will not be serialized at all, since only public properties are serialized. Demo fiddle #3 here.

(Adding [JsonIgnore] to the overridden property also prevents its serialization, however it would be necessary to do this in every derived class, which it appears you do not want to do.)

Original answer: This may be a limitation of System.Text.Json. I was able to reproduce your problem using the following code:

var derived = new Derived { VoteCount = 101 };

var json1 = JsonSerializer.Serialize(derived, typeof(Derived));
Console.WriteLine(json1); // {"VoteCount":101,"SortValue":{}}

var json2 = JsonSerializer.Serialize(derived, typeof(Base));
Console.WriteLine(json2); //{}

When an instance of Derived is serialized as type Derived, the SortValue property is included; but when serialized as type Base it is not. Demo fiddle #1 here.

But is this behavior intended? Neither the overview docs nor the JsonIgnoreAttribute docs discuss whether [JsonIgnore] is inherited, however the .Net 3.1 source code for JsonIgnoreAttribute as well as the current source show [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonIgnoreAttribute : JsonAttribute
{
    /// <summary>
    /// Initializes a new instance of <see cref="JsonIgnoreAttribute"/>.
    /// </summary>
    public JsonIgnoreAttribute() { }
}

Notice that AttributeUsageAttribute.Inherited is not set. Since the default value for Inherited is documented as follows:

true if the attribute can be inherited by derived classes and overriding members; otherwise, false. The default is true.

It seems that [JsonIgnore] should have been inherited by the overridden property. It is certainly inconsistent with both Json.NET and DataContractJsonSerializer, which respectively honor inheritance for Newtonsoft.Json.JsonIgnoreAttribute and IgnoreDataMemberAttribute as shown in demo fiddle #2 here. You might want to open an issue about this; at the minimum the documentation should be clarified.

Related