3

I have some object like this:

public class ResponseBase
{
    public string eventType { get; }
    public string eventSourceGuid { get; }
}

public class QueryDevicesResponse : ResponseBase
{
    public new static string eventType { get => "queryDevicesResponse"; }
    public new string eventSourceGuid { get => "0"; }
    public EventData eventData { get; set; }
}

eventType field is static because I'm trying to:

  • minimize lines of code
  • use it like "application-wide" string stored (in source code) together with DTO class definition, and use it in some switch and if statements without instantiating QueryDevicesResponse.

When I call:

QueryDevicesResponse queryDevicesResponse = QueryDevicesResponse.Mock();
JsonSerializer.Serialize.JsonSerializer.Serialize(queryDevicesResponse);

I'm getting JSON without eventType field. I guess this is because field is static.

Can I change JsonSerializer behavior to include also static fields?

This is similar question, but this is about Newtonsoft.Json:

Why can't JSON .Net serialize static or const member variables?

Alternatively:

How can I replace static modifier to get similar behavior and keep code small?

2
  • As far as I know there's no way (in .Net 3.x) to return a static property using System.Text.Json other than writing a custom JsonConverter for the containing class. If you want to use it like "application-wide" string stored (in source code), why not put it in a const statement? Or put all the eventType string values in const statements in some separate static class? Also, why not make the property virtual? Commented Nov 5, 2020 at 15:20
  • @dbc Well, I tried to keep design as close to POCO as possible. in the end I changed my design by removing static. Now my C# code uses class name in switch and if statements. Thanks for the "there's no way (in .Net 3.x)" information, that could be an answer I would accept. P.s. are we alone here? :D Commented Nov 5, 2020 at 23:05

1 Answer 1

0

As of .NET 7 you have several new options to handle this situation.

Can I change JsonSerializer behavior to include also static fields?

You can use a typeInfo modifier to customize your type's serialization contract and include static properties in the serialized output in a generic way.

First define the following modifier:

public static class JsonExtensions
{
    public static Action<JsonTypeInfo> AddStaticProperties(Type baseType, Type? attributeType = null) => typeInfo =>
    {
        if (typeInfo.Kind != JsonTypeInfoKind.Object)
            return;
        if (!typeInfo.Type.IsAssignableTo(baseType))
            return;
        foreach (var dotnetProperty in GetSerializableProperties(typeInfo.Type, BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy, baseType, attributeType))
        {
            var jsonName = dotnetProperty.GetJsonPropertyName(typeInfo.Options);
            var index = typeInfo.Properties.FindLastIndex(jp => jp.Name == jsonName);

            // If the existing JSON property was declared more deeply in the inheritance hierarchy than the reflected property, do not replace it.
            if (index >= 0
                && (typeInfo.Properties[index].GetDeclaringType()?.InheritanceDepth() >= dotnetProperty.DeclaringType?.InheritanceDepth()))
                continue;
            if (!(CreateStaticPropertyGetter(dotnetProperty) is {} getter))
                continue;
            var jsonProperty = typeInfo.CreateJsonPropertyInfo(dotnetProperty.PropertyType, jsonName);
            jsonProperty.AttributeProvider = dotnetProperty;
            jsonProperty.CustomConverter = dotnetProperty.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType is {} converterType
                ? (JsonConverter?)Activator.CreateInstance(converterType)
                : null;
            jsonProperty.Get = getter;
            if (index >= 0)
                typeInfo.Properties[index] = jsonProperty;
            else 
                typeInfo.Properties.Add(jsonProperty);
        }
    };

    static Func<object, object?>? CreateStaticPropertyGetter(PropertyInfo dotnetProperty)
    {
        if (dotnetProperty.DeclaringType == null || !(dotnetProperty.GetGetMethod() is {} getMethod))
            return null;
        if (!getMethod.IsStatic)
            throw new ArgumentException($"{dotnetProperty} is not static");
        var typedCreator = typeof(JsonExtensions).GetMethod(nameof(CreateStaticTypedPropertyGetter), BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
        var concreteTypedCreator = typedCreator = typedCreator!.MakeGenericMethod(dotnetProperty.PropertyType);
        return (Func<object, object?>?)concreteTypedCreator.Invoke(null, new object [] { getMethod });
    }

    static Func<object, object?> CreateStaticTypedPropertyGetter<TValue>(MethodInfo methodInfo)
    {
        var typedFunc = (Func<TValue?>)Delegate.CreateDelegate(typeof(Func<TValue?>), methodInfo);
        return (o => typedFunc());
    }

    static int FindLastIndex<T>(this IList<T> list, Predicate<T> match)
    {
        for (int i = list.Count - 1; i >= 0; i--)
            if (match(list[i]))
                return i;
        return -1;
    }
    
    static IEnumerable<PropertyInfo> GetSerializableProperties(Type type, BindingFlags flags, Type baseType, Type? attributeType)
    {
        var query = type.GetProperties(flags).Where(p => p.GetIndexParameters().Length == 0 && p.CanRead).Where(p => p.DeclaringType?.IsAssignableTo(baseType) == true);
        return attributeType == null ? query : query.Where(p => Attribute.IsDefined(p, attributeType)); 
    }

    static int InheritanceDepth(this Type type) => type.BaseTypesAndSelf().Count();

    static IEnumerable<Type> BaseTypesAndSelf(this Type type)
    {
        while (type != null)
        {
            yield return type;
            type = type.BaseType!;
        }
    }

    static string GetJsonPropertyName(this MemberInfo info, JsonSerializerOptions? options)
        => info.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? options?.PropertyNamingPolicy?.ConvertName(info.Name) ?? info.Name;
    
    static Type? GetDeclaringType(this JsonPropertyInfo property) => 
#if NET9_0_OR_GREATER
        // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives#conditional-compilation
        // https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.jsonpropertyinfo.declaringtype
        // DeclaringType first introduced in .NET 9
        property.DeclaringType;
#else
        (property.AttributeProvider as MemberInfo)?.DeclaringType;
#endif  
}

Then, to include public static properties of ResponseBase, set up your options as follows:

JsonSerializerOptions options = new()
{   // Add whatever standard options you need here:
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true,
};
options.TypeInfoResolver = 
    (options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
    .WithAddedModifier(JsonExtensions.AddStaticProperties(typeof(ResponseBase)));

And serialize a QueryDevicesResponse response = new() { /* ... */ }; using these options, you will get your static properties included:

{
  "eventSourceGuid": "0",
  "eventData": {},
  "eventType": "queryDevicesResponse"
}

Alternatively, if you would prefer to serialize all static properties marked with some attribute, say

[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple = false)]
public class JsonStaticIncludeAttribute : JsonAttribute;

pass typeof(object) as the base type, and typeof(JsonStaticIncludeAttribute) as the second argument:

options.TypeInfoResolver = 
    (options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
    .WithAddedModifier(JsonExtensions.AddStaticProperties(
        typeof(object), attributeType: typeof(JsonStaticIncludeAttribute)));

Caveats and notes:

  • Deserialization is not implemented.

  • Using member hiding rather than virtual overriding to replace the serialized property values looks fragile, because System.Text.Json is designed to serialize the declared type of objects, rather than the actual type as Json.NET does. Thus if you were to declare the value to be serialized as ResponseBase instead of QueryDevicesResponse, the base type properties will be emitted instead. I.e.:

    ResponseBase response = new QueryDevicesResponse() { /* ... */ };
    var json = JsonSerializer.Serialize(response, options);
    

    Results in

    {
      "eventType": null,
      "eventSourceGuid": null
    }
    

    This is not fixable using a typeInfo modifier, as the actual, concrete type is never passed in. This is especially likely to trip you up when working with polymorphic type hierarchies.

  • In my opinion, hiding a base type instance property (here public string eventType { get; }) with a derived type static property (here public new static string eventType => "queryDevicesResponse";) has a bit of a code smell.

  • The answer shows how to include static properties. If you also need static fields, you can get them using reflection. See How to get static fields from another class using reflection? and Get value of a public static field via reflection.

    For constants, see How can I get all constants of a type by reflection?

  • As this answer uses reflection, it will not work with source generation in Native AOT contexts.

Demo fiddle #1 here.

How can I replace static modifier to get similar behavior and keep code small?

I would suggest you simply make eventType and eventSourceGuid abstract or virtual and override them in all derived types. This avoids the fragility of member hiding and guarantees consumers of your type hierarchy will always see correct values. Next, to fulfill your requirement to be able to access the event name without creating an instance, you could create some interface with a static abstract EventTypeName property. Then implement that interface on all concrete types derived from ResponseBase making the eventType instance properties redirect to the static property. There's a single extra line of boilerplate, but it does allow you to access the static name in a generic way:

public abstract class ResponseBase
{
    public abstract string eventType { get; }
    public abstract string eventSourceGuid { get; }
}

public interface ITypedEventResponse
{
    public static abstract string EventTypeName { get; }
}

public class QueryDevicesResponse : ResponseBase, ITypedEventResponse
{
    public static string EventTypeName => "queryDevicesResponse";
    public override string eventType => EventTypeName; // The single extra line of boilerplate
    public override string eventSourceGuid => "0";
    public EventData eventData { get; set; }
}

The ITypedEventResponse interface allows you to do convenient things with constraints. For instance, if you want to build up a dictionary of event type names to types, you could do it like this:

public static class ResponseTypeData
{
    public static IDictionary<string, Type> ResponseTypes { get; } = new ConcurrentDictionary<string, Type>()
        .Register<QueryDevicesResponse>()
        .Register<SomeOtherResponse>();
        
    public static IDictionary<string, Type> Register<TResponseBase>(this IDictionary<string, Type> dict) 
        where TResponseBase : ResponseBase, ITypedEventResponse
    {
        dict.Add(TResponseBase.EventTypeName, typeof(TResponseBase));
        return dict;
    }
}

The where TResponseBase : ResponseBase, ITypedEventResponse constraint guarantees the specified type has its own EventTypeName static property. This approach works with reflection and with source generation, and completely avoids having to do tricky things with serialization.

Demo fiddle #2 here.

Finally, as an alternative, you could investigate System.Text.Json's built-in support for polymorphism which was also added in .NET 7. To get started, see this answer to Is polymorphic deserialization possible in System.Text.Json?.

Sign up to request clarification or add additional context in comments.

1 Comment

Since this question was unanswered despite having a decent number of views and upvotes, I went ahead and added this answer from the future.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.