nats-io/nats.net

Framework guidelines dictate that Enum names should be Pascal cased

Closed this issue · 9 comments

Framework guidelines dictate that Enum names should be Pascal cased -- then you could avoid the ugly at escape on explicit.

Originally posted by @oising in #212 (comment)

The only solution I can come up with is to create a convertor:

public class JsonStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
{
    public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (typeToConvert == typeof(MyEnum))
        {
            var type = reader.TokenType;
            if (type == JsonTokenType.String)
            {
                var stringValue = reader.GetString();

                switch (stringValue)
                {
                    case "explicit":
                        return (TEnum)(object)MyEnum.Explicit;
                    case "all":
                        return (TEnum)(object)MyEnum.All;
                    case "none":
                        return (TEnum)(object)MyEnum.None;
                }
            }

            return default;
        }
            
        throw new InvalidOperationException();
    }

    public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
    {
        if (value is MyEnum myEnum)
        {
            switch (myEnum)
            {
                case MyEnum.None:
                    writer.WriteStringValue("none");
                    break;
                case MyEnum.All:
                    writer.WriteStringValue("all");
                    break;
                case MyEnum.Explicit:
                    writer.WriteStringValue("explicit");
                    break;
            }
        }
        else
        {
            throw new InvalidOperationException();
        }
    }
}

@mtmk -- net8.0 natively has JsonStringEnumConverter<T> -- is this for net6?

yes but it seems to be ignoring the case options for me 😢
Also yes, we need to support net6.0 as well.

Okay, I definitely had it working in net6 -- let me get a spike going locally (console net6 app) to validate the settings and I'll get back to you here.

Alright, got it. This is targeting net6.0 but I'm referencing the 8.0.0 sys.text.json (as you probably are too)

// See https://aka.ms/new-console-template for more information

using System.Text.Json;
using System.Text.Json.Serialization;

// object to serialize
var shape = new Shape { Color = Color.Red };

// override the default enum naming policy by passing our own string enum converter instance
var context = new ShapeSerializationContext(new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
});

// we pass our custom context instead of ShapeSerializationContext.Default
var json = JsonSerializer.Serialize(shape, context.Shape);
Console.WriteLine(json);

public enum Color
{
    Red,
    Green,
    Blue
}

public class Shape
{
    public Color Color { get; set; }
}

[JsonSerializable(typeof(Shape))]
[JsonSerializable(typeof(Color))]
[JsonSourceGenerationOptions(UseStringEnumConverter = true)] // i.e. "red" not 0
public partial class ShapeSerializationContext : JsonSerializerContext
{
}

outputs: { "color": "red" }

Figuring this out the first time took me hours.

Here's the variant for net8.0 that will permit AOT publish:

// See https://aka.ms/new-console-template for more information

using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;

// object to serialize
var shape = new Shape { Color = Color.Red };

// override the default enum naming policy by passing our own string enum converter instance
var context = new ShapeSerializationContext(new JsonSerializerOptions
{
    // ensures "Color" property will be camel-cased
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    
    // use new generic version of JsonStringEnumConverter to permit AOT
    // need to also pass in the naming policy to ensure the enum values are camel-cased
    Converters = { new JsonStringEnumConverter<Color>(JsonNamingPolicy.CamelCase) }
});

Debug.Assert(JsonSerializer.IsReflectionEnabledByDefault == false);

// we pass our custom context instead of ShapeSerializationContext.Default
var json = JsonSerializer.Serialize(shape, context.Shape);
Console.WriteLine(json);

public enum Color
{
    Red,
    Green,
    Blue
}

public class Shape
{
    public Color Color { get; set; }
}

[JsonSerializable(typeof(Shape))]
[JsonSerializable(typeof(Color))]
[JsonSourceGenerationOptions(
    UseStringEnumConverter = true)] // i.e. "red" not 0
public partial class ShapeSerializationContext : JsonSerializerContext
{
}

My csproj:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
        <PublishAot>true</PublishAot>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="System.Text.Json" Version="8.0.0" />
    </ItemGroup>

</Project>

this is great. worked for me in my little test app. I think I can apply it now. thank you @oising!

And thank you for your great work giving us a top class dotnet client!