g.pickardou
g.pickardou

Reputation: 35913

How to serialize interface default property?

This question is resurrected, because I was wrong in comment "My bad". This issue is there, or I miss something. For popular demand here is a minimal reproducable example:

minimal reproducable example


I have an interface with a default property:

public interface INotification
{
    // ...
    Severity Severity { get; set; } // Severity is an Enum
    // ...
    public string SeverityName => Severity.ToString().ToLower();
}

I have an implementation class

public class Notification : INotification
{
    // ...
    public Severity Severity { get; set; }
    // ...
    // This class does not override default implementation of SeverityName
} 

Question

The Notification class does not have a SeverityName property... this was surprising, but I can live with that, accessing to the notification instance via the INotification interface.

However, I would like to serialize this class with System.Text.Json. How can I serialize SeverityName property too?

Upvotes: 3

Views: 926

Answers (1)

dbc
dbc

Reputation: 116980

This is not possible with System.Text.Json in .NET Core 3.1 other than by writing a custom JsonConverter for Notification. The serializer serializes the public properties of a type, but default interface properties are not implemented as class properties, instead they're implemented through some sort of extension mechanism. See the spec: default interface methods

Add support for virtual extension methods - methods in interfaces with concrete implementations. .

Only when the default interface property is overridden does an instance property actually get added to the implementing concrete type, and thereby become serializable.

For confirmation, if we modify the classes from your fiddle as follows:

public interface INotification
{
    Severity Severity { get; set; }
    string Message { get; set; }

    static string MakeDefaultSeverityName<TNotification>(TNotification notification) where TNotification : INotification => notification?.Severity.ToString().ToLower();

    public string SeverityName => MakeDefaultSeverityName(this);
}

public class Notification : INotification
{
    public Severity Severity { get; set; }
    public string Message { get; set; }
}

public class NotificationWithOverride : Notification
{
    public string SeverityName => INotification.MakeDefaultSeverityName(this);
}

And print out the properties and methods of both types using reflection:

Console.WriteLine("Properties of {0}: {1}", typeof(TNotification).Name, string.Join(", ", typeof(TNotification).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Select(p => p.Name)));
Console.WriteLine("Methods of {0}: {1}", typeof(TNotification).Name, string.Join(", ", typeof(TNotification).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Select(p => p.Name)));

We get the following results for Notification:

Properties of Notification: Severity, Message
Methods of Notification: get_Severity, set_Severity, get_Message, set_Message, GetType, MemberwiseClone, Finalize, ToString, Equals, GetHashCode

And for NotificationWithOverride:

Properties of NotificationWithOverride: SeverityName, Severity, Message
Methods of NotificationWithOverride: get_SeverityName, get_Severity, set_Severity, get_Message, set_Message, GetType, MemberwiseClone, Finalize, ToString, Equals, GetHashCode

Notice there's just no instance property or method corresponding to SeverityName in Notification -- but there is in NotificationWithOverride. Lacking a property to serialize, the serializer does not output a SeverityName value.

Notes:

  1. System.Text.Json is consistent with other serializers in this respect; both it and Json.NET generate exactly the same JSON:

    • {"Severity":0,"Message":"Message"} for Notification.
    • {"SeverityName":"info","Severity":0,"Message":"Message"} for NotificationWithOverride.
  2. With Json.NET it might be possible to create a custom contract resolver that automatically adds in the missing default properties, but System.Text.Json does not have public access to its contract resolver; for confirmation see the question System.Text.Json API is there something like IContractResolver to which the answer is, not currently.

  3. In c# 8.0 it is not possible for to override a default interface method in a concrete class and call the base interface method. See How can I call the default method instead of the concrete implementation, Making member virtual prevents calling default interface implementation and causes StackOverflowException in C# 8 and Default interface implementations and base() calls for details.

    To work around this I added a static interface method to INotification that encapsulates the default logic, then called it from both the default interface instance method and the override class method.

A demo fiddle that shows the above can be found here.

Upvotes: 2

Related Questions