2

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
dbc
  • 104,963
  • 20
  • 228
  • 340
Vincent
  • 510
  • 1
  • 5
  • 23
  • 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... – Jon Skeet Aug 16 '23 at 15:34
  • 1
    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. – Serge Aug 16 '23 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) ? – Vincent Aug 17 '23 at 05:19
  • @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?](https://stackoverflow.com/q/24336597). – dbc Aug 17 '23 at 05:36

1 Answers1

3

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Nice ! A huge thank to you ! The first `public static Action AddOptInConstMembers(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 Aug 17 '23 at 05:22
  • 1
    @Vincent - I wrote that in case somebody wanted to include constants just for one specific type rather than for all types. – dbc Aug 17 '23 at 05:32