Search code examples
asp.netswagger-uinswag

Enum property in DTO is generated as string in OpenAPI specification


Problem Description

I'm utilizing NSwag to generate the OpenAPI specification for my API. The project I'm currently engaged with was initially constructed using ASP.NET 5.0, and recently I made the decision to upgrade it to ASP.NET 8.0. Following the upgrade of all associated libraries, I promptly encountered an issue with one of the endpoints.

This specific endpoint is designed to accept a DTO containing an IFormFile and an enumeration property named FileType. In the controller code, the DTO parameter containing both these properties is annotated with the [FromForm] attribute. However, upon transitioning to ASP.NET 8.0, I observed that this enum property is generated as an integer, whereas in ASP.NET 5.0 it behaved differently. enter image description here

The OpenAPI specification describes FileType as integer type and also generates enum and x-enumNames attributes: enter image description here

enter image description here

After I migrated project to ASP.NET 8.0 the enum property suddenly became string and enum and x-enumNames don't get generated anymore: enter image description here enter image description here

Diving Deeper

I spent some time digging into NSwag issues trying to find a solution, but unfortunately, I didn't find anything useful. So, I decided to do some debugging. Turns out, NSwag uses the ASP.NET implementation of IApiDescriptionGroupCollectionProvider and IApiDescriptionProvider to describe the API.

After looking into it further, I noticed something interesting. The ApiDescription object contains a bunch of ApiParameterDescription instances that describe the API parameters. What caught my attention was the difference in the Type property between ASP.NET 5.0 and 8.0. In the older version, it showed the enum type like FileType, but in the newer one, it just said string.

As I kept digging, I found out that the DefaultApiDescriptionProvider.CreateResult method is where things differ. Let me break down the changes in this method below:

.net 5.0

    private ApiParameterDescription CreateResult(
        ApiParameterDescriptionContext bindingContext,
        BindingSource source,
        string containerName)
    {
        return new ApiParameterDescription()
        {
            ...
            Type = bindingContext.ModelMetadata.ModelType,
            ...
        };
    }

.net 8.0

    private ApiParameterDescription CreateResult(
        ApiParameterDescriptionContext bindingContext,
        BindingSource source,
        string containerName)
    {
        return new ApiParameterDescription()
        {
            ...
            Type = GetModelType(bindingContext.ModelMetadata),
            ...
        };
    }
    
    private static Type GetModelType(ModelMetadata metadata)
    {
        // IsParseableType || IsConvertibleType
        if (!metadata.IsComplexType)
        {
            return EndpointModelMetadata.GetDisplayType(metadata.ModelType);
        }

        return metadata.ModelType;
    }

    public static Type GetDisplayType(Type type)
    {
        var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
        return underlyingType.IsPrimitive
            // Those additional types have TypeConverter or TryParse and are not primitives
            // but should not be considered string in the metadata
            || underlyingType == typeof(DateTime)
            || underlyingType == typeof(DateTimeOffset)
            || underlyingType == typeof(DateOnly)
            || underlyingType == typeof(TimeOnly)
            || underlyingType == typeof(TimeSpan)
            || underlyingType == typeof(decimal)
            || underlyingType == typeof(Guid)
            || underlyingType == typeof(Uri) ? type : typeof(string);
    }

As it can be seen, things have changed in the implementation, especially in .NET 8.0, where any enum is now treated as a string parameter. But I really need to keep this parameter as an integer because switching it to a string would mess up my API and cause problems. So, if you've got any ideas on how to deal with this, I'm all ears!


Solution

  • I solved the issue by providing a custom implementation of the IApiDescriptionGroupCollectionProvider where I adjusted the code of the GetCollection method:

        var context = new ApiDescriptionProviderContext(actionDescriptors.Items);
    
        foreach (var provider in _apiDescriptionProviders)
        {
            provider.OnProvidersExecuting(context);
        }
    
        // Added this block to change the parameter type for enumeration parameters back to their original type
        foreach (var param in context.Results.SelectMany(r => r.ParameterDescriptions))
        {
            if (param.ModelMetadata.ModelType.IsEnum)
            {
                param.Type = param.ModelMetadata.ModelType;
            }
        }
    
        for (var i = _apiDescriptionProviders.Length - 1; i >= 0; i--)
        {
            _apiDescriptionProviders[i].OnProvidersExecuted(context);
        }
    
        var groups = context.Results
            .GroupBy(d => d.GroupName)
            .Select(g => new ApiDescriptionGroup(g.Key, g.ToArray()))
            .ToArray();
    
        return new ApiDescriptionGroupCollection(groups, actionDescriptors.Version);