4

In .NET 7, is there a way to serialize a const?

internal sealed class CreateResourceCommand : BaseCommand
{
    public const string CommandName = "CreateResourceCommand";
}

I have many Commands derived from a base who are sent to a message queue. The remote consumer will deserialize the payload and not get the CommandName.

It is straightforward with Newtonsoft.Json but I can't make it work with System.Text.Json.Serialization.

I tried :

  • [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
  • a JsonConverter but the const won't appear in the value
4
  • 2
    It isn't logically part of an object, so it doesn't surprise me that it's not serialized. You might want to have an abstract or virtual property in BaseCommand, and then implement it as public string Name => CommandName; as a public property. Although if you're going to deserialize, you have to wonder where it'll end up... Commented Aug 16, 2023 at 15:34
  • 2
    Doesn't make any sense to serialize and doesn't serialize const since it is part of class, not of class instance. IMHO In this case it makes sense to use read only and create a constructor code to init it. Commented Aug 16, 2023 at 17:41
  • @JonSkeet @Serge Thank you both. Legacy code using NewtonSoft.Json and trying to migrate to System.Text.Json. I did consider your approach @JonSkeet but I needed to keep the const ofr other matters. Before@dbc's answer, I would have got with yours @Serge. Is NewtonSoft.Json wrong to serialize them by default (of just with JsonInclude.. I don't recall exactly) ? Commented Aug 17, 2023 at 5:19
  • 1
    @Vincent - Json.NET only serializes public or private const fields when marked with [JsonProperty], See How to serialize static or const member variables using JSON.NET?. Commented Aug 17, 2023 at 5:36

1 Answer 1

4

As mentioned in comments by Serge, serializing const values is a bit of an odd thing to do, and is apparently not implemented out of the box even if you apply [JsonInclude]. Thus you're going to need to create a typeInfo modifier to serialize them.

First define the following modifiers to force serialization of const values when some attribute TOptInAttribute is applied:

public static partial class JsonExtensions
{
    // Include opted-in constants for the specified type.
    public static Action<JsonTypeInfo> AddOptInConstMembers<TOptInAttribute>(Type type) where TOptInAttribute : System.Attribute =>
        typeInfo =>
        {
            if (typeInfo.Type == type)
                AddOptInConstMembers<TOptInAttribute>(typeInfo);
        };

    // Include opted-in constants for all types.
    public static void AddOptInConstMembers<TOptInAttribute>(JsonTypeInfo typeInfo) where TOptInAttribute : System.Attribute
    {
        if (typeInfo.Kind != JsonTypeInfoKind.Object)
            return;
        foreach (var field in typeInfo.Type.GetConstants().Where(f => Attribute.IsDefined(f, typeof(TOptInAttribute))))
        {
            var name = field.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? typeInfo.Options.PropertyNamingPolicy?.ConvertName(field.Name) ?? field.Name;
            var value = field.GetValue(null); // field.GetValue(null); returns enums as enums rathen raw integers.
            var propertyInfo = typeInfo.CreateJsonPropertyInfo(value?.GetType() ?? field.FieldType, name);
            propertyInfo.Get = (o) => value;
            propertyInfo.CustomConverter = field.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType is {} converterType
                ? (JsonConverter?)Activator.CreateInstance(converterType)
                : null;
            typeInfo.Properties.Add(propertyInfo);
        }
    }

    static IEnumerable<FieldInfo> GetConstants(this Type type) =>
        // From the answer https://stackoverflow.com/a/10261848
        // By https://stackoverflow.com/users/601179/gdoron
        // To https://stackoverflow.com/questions/10261824/how-can-i-get-all-constants-of-a-type-by-reflection
        type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarchy)
        .Where(fi => fi.IsLiteral && !fi.IsInitOnly);
}

Then apply your chosen TOptInAttribute attribute to the const fields to be serialized. It could be your own custom attribute or [JsonInclude] as per your preference:

internal sealed class CreateResourceCommand : BaseCommand
{
    [JsonInclude]
    public const string CommandName = "CreateResourceCommand";
}

Finally, to serialize const fields with that attribute applied (here [JsonInclude]), use the following options:

var command = new CreateResourceCommand();

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers = { JsonExtensions.AddOptInConstMembers<JsonIncludeAttribute> },
    },
};
var json = JsonSerializer.Serialize(command, options);

Console.WriteLine(json); // Prints {"CommandName":"CreateResourceCommand"}
Assert.AreEqual("""{"CommandName":"CreateResourceCommand"}""", json);

Of course the const values are read-only and will not be deserialized.

Demo fiddle here.

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

2 Comments

Nice ! A huge thank to you ! The first public static Action<JsonTypeInfo> AddOptInConstMembers<TOptInAttribute>(Type type) where TOptInAttribute : System.Attribute => doesn't seem to be needed though. I got an unused code warning and it works alright without it. Thanks for the reference of GetConstants
@Vincent - I wrote that in case somebody wanted to include constants just for one specific type rather than for all types.

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.