6

I am trying to get the enum to display friendly name from the description (or any other attribute) in swagger and on response. Also trying to parse the friendly name set on the body/querystring in the controller action without trying a 400 BadRequest or any kinda validation error. What I have also noticed is that the custom generic JsonConverter I have is also not working properly. the ReadJson() method isn't being called at all. How can I get this to work?

[JsonConverter(typeof(JsonEnumConverter<SortDirectionType>))]
public enum SortDirectionType
{
    [Description("asc")]
    ASCENDING,
    [Description("desc")]
    DESCENDING
}

I'm trying to get swagger-ui to show asc & desc as the values in the dropdown as opposed to ASCENDING & DESCENDING. It means that I cannot use c.DescribeAllEnumsAsStrings(). If I dont use it then the dropdown shows 0,1 as it should to represent the enum member values. Now I could use [EnumMember(Value="asc"] attribute instead of [Description("asc")] attribute. However then two things are happening:

  1. Either swagger-ui throws a client validation and highlights the field with red line around it
  2. Or if I try to call the endpoint outside of swagger it returns a 400 error.
  3. I'm able to use the 0,1 or ASCENDING, DESCENDING values successfully within the action [FromBody] argument. However that's not what is expected. I'm going to receive asc, desc and want to be able to parse that successfully on the body model and map it to the enum property. On the flip side when the json renders it should render the friendly name.

Additional Code:

public class JsonEnumConverter<T> : JsonConverter where T : struct, IComparable, IConvertible, IFormattable
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(T);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var type = typeof(T);

        if (!type.IsEnum) throw new InvalidOperationException();

        var enumDescription = (string)reader.Value;

        return enumDescription.GetEnumValueFromDescription<T>();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var type = typeof(T);

        if (!type.IsEnum) throw new InvalidOperationException();

        if (value != null)
        {
            if (value is Enum sourceEnum)
            {
                writer.WriteValue(sourceEnum.GetDescriptionFromEnumValue());
            }
        }
    }
}

public static class EnumExtensions
{
    public static string GetDescriptionFromEnumValue(this Enum @enum)
    {
        FieldInfo fi = @enum.GetType().GetField(@enum.ToString());

        DescriptionAttribute[] attributes =
            (DescriptionAttribute[])fi.GetCustomAttributes(
            typeof(DescriptionAttribute),
            false);

        if (attributes != null &&
            attributes.Length > 0)
            return attributes[0].Description;
        else
            return @enum.ToString();
    }

    public static T GetEnumValueFromDescription<T>(this string description)
    {
        var type = typeof(T);

        if (!type.IsEnum)
            throw new InvalidOperationException();

        foreach (var field in type.GetFields())
        {
            if (Attribute.GetCustomAttribute(field,
                typeof(DescriptionAttribute)) is DescriptionAttribute attribute)
            {
                if (attribute.Description == description)
                    return (T)field.GetValue(null);
            }
            else
            {
                if (field.Name == description)
                    return (T)field.GetValue(null);
            }
        }

        throw new ArgumentException($"No matching value for enum {nameof(T)} found from {description}.",$"{nameof(description)}"); // or return default(T);
    }
}

https://github.com/domaindrivendev/Swashbuckle/issues/1318

BRBdot
  • 758
  • 2
  • 8
  • 20

2 Answers2

1

This is what did it for me:

  services.AddSwaggerGen(c => {
  c.DescribeAllEnumsAsStrings();
});

I found the it here

1

I came up with a hack here. It may not necessarily be the solution for you however if you can make it work then I'm happy I could help.

Ended up changing the enum format to string instead of default dropdown. This way I could send asc/desc values to the api. Swagger accepted the values and didnt throw validation errors. And the converter I wrote on the .net core api was also able to convert them nicely.

c.MapType<SortDirectionType>(() => new Schema { Type = "string", Format = "string" });

In addition to this, you may also want to disable default behavior of asp.net core 2.2 validation that kicks in automatically. Not sure why they chose to set that as default behavior.

services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});
BRBdot
  • 758
  • 2
  • 8
  • 20
  • Does this approach work with other custom type as well? Other than enum – liang Nov 01 '19 at 09:29
  • I believe so. Can you elaborate on what you mean by custom type? All its doing is telling swagger to treat the underlying type as string on the ui – BRBdot Nov 01 '19 at 21:45
  • custom type as in, public class Foo{ public int A {get;set;} public int B {get;set;} }. I tried to use `MapType`, doesn't seem to show as string in documentation. I end up using type converter suggested in https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/456 – liang Nov 04 '19 at 08:12