azure-functions-openapi-extension icon indicating copy to clipboard operation
azure-functions-openapi-extension copied to clipboard

Add [JsonConverter(typeof(StringEnumConverter))] to Enums outside of my Assembly

Open NickSpaghetti opened this issue 4 years ago • 4 comments

Hey,

I'm running into an issue with enums showing up as numbers in the swagger UI. According to the documentation I need to add [JsonConverter(typeof(StringEnumConverter))] to the enum file. However, for enums outside of my assembly or in Nuget packages I can not add that attribute. I tried doing it via reflection in the Azure Functions Startup.cs. see code below:

 // add [JsonConverter(typeof(StringEnumConverter))]
 var nugetAssembly = typeof(IBaseModel).Assembly;
 var enumTypes = nugetAssembly.GetTypes().Where(t => t.IsEnum).ToList();
 enumTypes.ForEach(e => TypeDescriptor.AddAttributes(e, new JsonConverterAttribute(e)));

I confirmed by printing out the Attributes that the JsonConverterAttribute is in the enum in that assembly, but the swagger documentation still shows them as ints instead of string.

If this is a known bug please let me know.

Thanks!

NickSpaghetti avatar Oct 15 '21 20:10 NickSpaghetti

I would also like for this extension to respect the [JsonConverter(typeof(StringEnumConverter))] attribute on properties. For example:

public enum Mode
{
    [EnumMember(Value = "novice")]
    Novice = 0,
    [EnumMember(Value = "intermediate")]
    Intermediate = 1,
    [EnumMember(Value = "advanced")]
    Advanced = 2
}

public class Model
{
    [OpenApiProperty(Description = "The mode")]
    [JsonConverter(typeof(StringEnumConverter))]
    public Mode mode { get; set; }
}

Current output: Array [0, 1, 2] Expected output: Array [novice, intermediate, advanced]

josmithua avatar Nov 19 '21 01:11 josmithua

I would also like to use the StringEnumConverter. With the exception of having to declare the JsonConverter attribute on the enum properties. I would rather just setup the serializer settings with the StringEnumConverter. E.g.

using Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Extensions;
...
var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(worker =>
    {
        //Taking the defaults from:
        //https://github.com/Azure/azure-functions-openapi-extension/blob/main/src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/Extensions/FunctionsWorkerApplicationBuilderExtensions.cs
        var settings = NewtonsoftJsonObjectSerializer.CreateJsonSerializerSettings();
        settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        settings.NullValueHandling = NullValueHandling.Ignore;
        
        //with one additional preference of mine
        settings.Converters.Add(new StringEnumConverter());

        worker.UseNewtonsoftJson(settings);
    })
    .ConfigureOpenApi()

tyson-benson avatar Apr 28 '22 12:04 tyson-benson

+1 I had some auto generated code with NSwag and was a pain to add StringEnumConverter attribute to Enum each time code is refreshed.

I ended up writing document filter using reflection as a workaround (NOT IDEAL):

The problem: The enums were auto generated and I cannot keep adding StringEnumCovertor attribute each time the code was refreshed. Auto-generated code:

public enum Status
{
    [System.Runtime.Serialization.EnumMember(Value = @"new")]
    New = 0,

    [System.Runtime.Serialization.EnumMember(Value = @"confirmed")]
    Confirmed = 1,

    [System.Runtime.Serialization.EnumMember(Value = @"processing")]
    Processing = 2
}

public partial class Order 
{
	/// <summary>Status</summary>
	[Newtonsoft.Json.JsonProperty("status", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
	[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
	public Status Status { get; set; }
}

Solution:

public OpenApiConfigurationOptions()
{
	DocumentFilters.Add(new OpenApiEnumAsStringsDocumentFilter());
}

public class OpenApiEnumAsStringsDocumentFilter : IDocumentFilter
{
	private const string YourNamespace = "your.namespace";
	private const string EnumDefaultMemberValue = "value__";
	private const string StringSchemaType = "string";

	public void Apply(IHttpRequestDataObject request, OpenApiDocument document)
	{
		var assemblyTypes = Assembly
			.GetExecutingAssembly()
			.GetTypes()
			.Where(x => !string.IsNullOrEmpty(x.FullName) && x.FullName.StartsWith(YourNamespace, StringComparison.InvariantCulture));

		// Loop all DTO classes
		foreach (var schema in document.Components.Schemas)
		{
			foreach (var property in schema.Value.Properties)
			{
				if (property.Value.Enum.Any())
				{
					var schemaType = assemblyTypes.SingleOrDefault(t => t.Name.Equals(schema.Key, StringComparison.InvariantCultureIgnoreCase));
					if (schemaType == null)
						continue;

					var enumType = schemaType.GetProperty(string.Concat(property.Key[0].ToString().ToUpper(), property.Key.AsSpan(1))).PropertyType;

					UpdateEnumValuesAsString(property.Value, enumType);
				}
			}
		}

		// Loop all request parameters
		foreach (var path in document.Paths)
		{
			foreach (var operation in path.Value.Operations)
			{
				foreach (var parameter in operation.Value.Parameters)
				{
					if (parameter.Schema.Enum.Any())
					{
						var enumType = assemblyTypes.SingleOrDefault(t => t.Name.Equals(parameter.Name, StringComparison.InvariantCultureIgnoreCase));
						if (enumType == null)
							continue;

						UpdateEnumValuesAsString(parameter.Schema, enumType);
					}
				}
			}
		}
	}

	private static void UpdateEnumValuesAsString(OpenApiSchema schema, Type enumType)
	{
		schema.Enum.Clear();
		enumType
			.GetTypeInfo()
			.DeclaredMembers
			.Where(m => !m.Name.Equals(EnumDefaultMemberValue, StringComparison.InvariantCulture))
			.ToList()
			.ForEach(m =>
			{
				var attribute = m.GetCustomAttribute<EnumMemberAttribute>(false);
				schema.Enum.Add(new OpenApiString(attribute.Value));
			});
		schema.Type = StringSchemaType;
		schema.Default = schema.Enum.FirstOrDefault();
		schema.Format = null;
	}
}

cloud-devlpr avatar Jun 28 '22 15:06 cloud-devlpr