azure-functions-openapi-extension
azure-functions-openapi-extension copied to clipboard
Add [JsonConverter(typeof(StringEnumConverter))] to Enums outside of my Assembly
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!
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]
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()
+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;
}
}