domaindrivendev / Swashbuckle.AspNetCore

Swagger tools for documenting API's built on ASP.NET Core
MIT License
5.2k stars 1.29k forks source link

Nullable Enums don't appear as "nullable: true" in swagger.json #2378

Closed feliceiorillo closed 1 year ago

feliceiorillo commented 2 years ago

I am running .NET 6.0 ASP.Net Core Web Api with SwashBuckle.AspNetCore version 6.3.0 (currently last stable)

The swagger.json doesn't show nullable: true where there is a nullable enum in a dto class

{ "openapi": "3.0.1", "info": { "title": "TestBug", "version": "1.0" }, "paths": { "/api/My": { "get": { "tags": [ "My" ], "responses": { "200": { "description": "Success", "content": { "text/plain": { "schema": { "$ref": "#/components/schemas/MyDTO" } }, "application/json": { "schema": { "$ref": "#/components/schemas/MyDTO" } }, "text/json": { "schema": { "$ref": "#/components/schemas/MyDTO" } } } } } } } }, "components": { "schemas": { "MyDTO": { "type": "object", "properties": { "myEnum": { "$ref": "#/components/schemas/MyEnum" }, "myNullableEnum": { "$ref": "#/components/schemas/MyNullableEnum" }, "myNullableInt": { "type": "integer", "format": "int32", "nullable": true } }, "additionalProperties": false }, "MyEnum": { "enum": [ 0, 1, 2 ], "type": "integer", "format": "int32" }, "MyNullableEnum": { "enum": [ 0, 1, 2 ], "type": "integer", "format": "int32" } } } }

Steps to reproduce:

image image image

I am aware of using "UseAllOfToExtendReferenceSchemas", but it's not suitable in my case, since it's breaking other logics.

domaindrivendev commented 2 years ago

As per the JSON Schema specification, you can’t assign additional properties on a “reference” schema. That is, if a schema instance uses “$ref”, then it cannot have other properties assigned, including “nullable”. So, you have to apply “UseAllOfToExtendReferenceSchemas” to do this while maintaining a valid Open API / Swagger definition. You should look into what that’s breaking for you because it shouldn’t.

feliceiorillo commented 2 years ago

Thanks for your reply! I understand what you mean. But, is there any possibility, even changing the code of this nuget, to achieve my goal? I know this would be against the JSON Schema specification, but I think it's possible. Can you show me where is the code to change to do that?

anoordover commented 2 years ago

I have the same issue using NewtonSoft. I tried to make an unittest in a clone of this repository to reproduce the error but I couldn't reproduce this. I use dotnet core 3.1 with version 6.3 of Swashbuckle.AspNetCore. As a work-around is added a MapType<> for the enum that is used in the nullable property.

feliceiorillo commented 2 years ago

I have the same issue using NewtonSoft. I tried to make an unittest in a clone of this repository to reproduce the error but I couldn't reproduce this. I use dotnet core 3.1 with version 6.3 of Swashbuckle.AspNetCore. As a work-around is added a MapType<> for the enum that is used in the nullable property.

Hi, thanks for your suggestion. I have something like 30 different enums, not so suitable solution in my case...

anoordover commented 2 years ago

I committed the unittest (https://github.com/anoordover/Swashbuckle.AspNetCore/commit/c11f3bcbcdcd319931183a839149d7c2b7b54d3f)... Oeps... committed my experiment. Now with the correct code using a nullable enum: https://github.com/anoordover/Swashbuckle.AspNetCore/commit/d4d51c9cd10f3761f4b7490729b9049fff69d7b4 B.t.w. the unittest fails on my PC

anoordover commented 2 years ago

Maybe there is a work-around. Debugging my tests stopped on line 224 in SchemaGenerator. On that line a property UseInlineDefinitionsForEnums is used to decide to return the definition for the enum inline or as a ref. I will try this in my own solution and let you know.

anoordover commented 2 years ago

Yes! This work-around works: services.AddSwaggerGen(options => { options.EnableAnnotations(); options.UseInlineDefinitionsForEnums(); }

But still... on line 224 the definition should be inline when a property is nullable.

anoordover commented 2 years ago

I think this is a complex fix... A DataContract gets created based on the type of the property. This DataContract object doesn't seem to contain information about "nullable" of the property.

anoordover commented 2 years ago

Yes! This work-around works: services.AddSwaggerGen(options => { options.EnableAnnotations(); options.UseInlineDefinitionsForEnums(); }

But still... on line 224 the definition should be inline when a property is nullable.

Adding UseAllOfToExtendReferenceSchemas() instead of UseInlineDefinitionsForEnums() also works as a work-around...

feliceiorillo commented 2 years ago

UseAllOfToExtendReferenceSchemas breaks my client generation...

Yes! This work-around works: services.AddSwaggerGen(options => { options.EnableAnnotations(); options.UseInlineDefinitionsForEnums(); } But still... on line 224 the definition should be inline when a property is nullable.

Adding UseAllOfToExtendReferenceSchemas() instead of UseInlineDefinitionsForEnums() also works as a work-around...

anoordover commented 2 years ago

@feliceiorillo did you also try UseInlineDefinitionsForEnums()?

domaindrivendev commented 1 year ago

Closing as the solution (i.e. to use UseAllOfToExtendReferenceSchemas) has already been provided. If that's breaking client generators then you should create an issue with them instead of SB.

You could also try UseInlineDefinitionsForEnums as suggested by @anoordover

vadymberkut-unicreo commented 1 year ago

Solution that worked for me.

/// <summary>
    /// Updates already generated schema by marking $ref enum properties in it as nullable 
    /// if the orginal class property is of type nullable enum type.
    /// NB: schema filter can modify Schemas after they're initially generated.
    /// </summary>
    public class NullableEnumSchemaFilter : ISchemaFilter
    {
        public void Apply(OpenApiSchema schema, SchemaFilterContext context)
        {
            var isReferenceType =
                TypeHelper.IsReference(context.Type) &&
                !TypeHelper.IsCLR(context.Type) &&
                !TypeHelper.IsMicrosoft(context.Type);
            if(!isReferenceType) { return; }

            var bindingFlags = BindingFlags.Public | BindingFlags.Instance;
            var members = context.Type.GetFields(bindingFlags).Cast<MemberInfo>()
                .Concat(context.Type.GetProperties(bindingFlags))
                .ToArray();
            var hasNullableEnumMembers = members.Any(x => TypeHelper.IsNullableEnum(x.GetMemberType()));
            if (!hasNullableEnumMembers) { return; }

            schema.Properties.Where(x => !x.Value.Nullable).ForEach(property =>
            {
                var name = property.Key;
                var possibleNames = new string[]
                {
                    name,
                    TextCaseHelper.ToPascalCase(name),
                }; // handle different cases
                var sourceMember = possibleNames
                    .Select(n => context.Type.GetMember(n, bindingFlags).FirstOrDefault())
                    .Where(x => x != null)
                    .FirstOrDefault();
                if (sourceMember == null) { return; }

                var sourceMemberType = sourceMember.GetMemberType();
                if (sourceMemberType == null || !TypeHelper.IsNullableEnum(sourceMemberType)) { return; }

                // manual nullability fixes
                if (property.Value.Reference != null)
                {
                    // option 1 - OpenAPI 3.1
                    // https://stackoverflow.com/a/48114924/5168794
                    //property.Value.AnyOf = new List<OpenApiSchema>()
                    //{
                    //    new OpenApiSchema
                    //    {
                    //        Type = "null",
                    //    },
                    //    new OpenApiSchema
                    //    {
                    //        Reference = property.Value.Reference,
                    //    },
                    //};
                    // property.Value.Reference = null;

                    // option 2 - OpenAPI 3.0
                    // https://stackoverflow.com/a/48114924/5168794
                    property.Value.Nullable = true;
                    property.Value.AllOf = new List<OpenApiSchema>()
                    {
                        new OpenApiSchema
                        {
                            Reference = property.Value.Reference,
                        },
                    };
                    property.Value.Reference = null;

                    // option 3 - OpenAPI 3.0
                    // https://stackoverflow.com/a/23737104/5168794
                    //property.Value.OneOf = new List<OpenApiSchema>()
                    //{
                    //    new OpenApiSchema
                    //    {
                    //        Type = "null",
                    //    },
                    //    new OpenApiSchema
                    //    {
                    //        Reference = property.Value.Reference,
                    //    },
                    //};
                    //property.Value.Reference = null;
                }
            });
        }
    }
public static class TypeHelper
    {
        /// <summary>
        /// Checks if type is CLR type.
        /// </summary>
        public static bool IsCLR(Type type) => type.Assembly == typeof(int).Assembly;

        /// <summary>
        /// Checks if type is Microsoft type.
        /// </summary>
        public static bool IsMicrosoft(Type type) => type.Assembly.FullName?.StartsWith("Microsoft") ?? false;

        /// <summary>
        /// Checks if type is value type.
        /// </summary>
        public static bool IsValue(Type type) => type.IsValueType;

        /// <summary>
        /// Checks if type is reference type.
        /// </summary>
        public static bool IsReference(Type type) => !type.IsValueType && type.IsClass;

        /// <summary>
        /// Checks if property type is nullable reference type.
        /// NB: Reflection APIs for nullability information are available from .NET 6 Preview 7.
        /// </summary>
        public static bool IsNullableReferenceProperty(PropertyInfo property) => 
            new NullabilityInfoContext().Create(property).WriteState is NullabilityState.Nullable;

        /// <summary>
        /// Checks if type is enum type.
        /// </summary>
        public static bool IsEnum(Type type) => type.IsEnum || (Nullable.GetUnderlyingType(type)?.IsEnum ?? false);

        /// <summary>
        /// Checks if type is nullable enum type.
        /// </summary>
        public static bool IsNullableEnum(Type type) => Nullable.GetUnderlyingType(type)?.IsEnum ?? false;

        /// <summary>
        /// Checks if type is not nullable enum type.
        /// </summary>
        public static bool IsNotNullableEnum(Type type) => IsEnum(type) && !IsNullableEnum(type);
    }

Based on: