2

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;
}
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
Noah Stahl
  • 6,905
  • 5
  • 25
  • 36

1 Answers1

1

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Great info, thanks! To confirm, is it expected that an inheritable attribute should work for a derived override of abstract property? (in C# generally) – Noah Stahl Aug 28 '20 at 01:04
  • @NoahStahl - In general that should be controlled by [`AttributeUsageAttribute.Inherited`](https://learn.microsoft.com/en-us/dotnet/api/system.attributeusageattribute.inherited?view=netcore-3.1) applied to the attribute, which has a default value of `true`. The question [linked to](https://stackoverflow.com/questions/1240960/how-does-inheritance-work-for-attributes) in comments discusses this. – dbc Aug 28 '20 at 01:25
  • @NoahStahl - That being said, it is *possible* using reflection to get only the directly applied attributes, so if one is writing code that uses reflection extensively (which a serializer certainly does) then one can avoid that standard if one wants. – dbc Aug 28 '20 at 01:26
  • 1
    I just found this in the repo. Looks like this is intended by .NET team? https://github.com/dotnet/runtime/issues/39790#issuecomment-663055706 – Noah Stahl Aug 28 '20 at 01:44
  • And there's a section further down in the docs page that discusses how things work in derived types. Seems like this is the reason for the behavior. https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to#serialize-properties-of-derived-classes – Noah Stahl Aug 28 '20 at 01:50