Nullable property is not presented in the Swashbuckle.AspNetCore openapi schema properly
Asked Answered
P

3

10

Using Swashbuckle.AspNetCore v6.0.7 in an asp.net core project net5.0.

Let say I have models like these:

public enum MyEnum
{
  A, B
}

and

public class MyModel 
{
   public MyEnum MyEnum { get; set; }
   public MyEnum? MyEnum2 { get; set; }
}

and the swagger schema is generated like this:

"MyEnum": {
        "enum": [
          "A",
          "B"
        ],
        "type": "string"
      },
      "MyModel": {
        "type": "object",
        "properties": {
          "myEnum": {
            "$ref": "#/components/schemas/MyEnum"
          },
          "myEnum2": {
            "$ref": "#/components/schemas/MyEnum"
          }
        },
        "additionalProperties": false
      }

As you can see, there is no difference between MyEnum and MyEnum? in the open-API JSON schema!

And seems nullable enum is not presented in the schema properly.

Does anyone have any idea how can I fix this?

Best

Prostatectomy answered 2/3, 2021 at 15:16 Comment(1)
You can refer to the link,and try to use UseAllOfToExtendReferenceSchemas() as domaindrivendev said.Florrieflorry
P
21

As Yiyi You suggested, I called UseAllOfToExtendReferenceSchemas in SwaggerGenOptions like this:

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

and now the schema is generated like:

"MyEnum": {
        "enum": [
          "A",
          "B"
        ],
        "type": "string"
      },
      "MyModel": {
        "type": "object",
        "properties": {
          "myEnum": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MyEnum"
              }
            ]
          },
          "myEnum2": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MyEnum"
              }
            ],
            "nullable": true
          }
        },
        "additionalProperties": false
      },

and there is "nullable": true for the type MyEnum?.

You can find more information here.

Prostatectomy answered 3/3, 2021 at 12:14 Comment(0)
R
6

The following worked well for me:

_ = services.AddSwaggerGen(c =>
{
    // Enables support for nullable object properties
    c.UseAllOfToExtendReferenceSchemas();
    // Enable detection of non nullable reference types to set Nullable flag accordingly on schema properties
    c.SupportNonNullableReferenceTypes();
})
Romaic answered 20/6, 2023 at 12:5 Comment(0)
R
0

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://mcmap.net/q/144744/-how-to-specify-a-property-can-be-null-or-a-reference-with-swagger
                    //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://mcmap.net/q/144744/-how-to-specify-a-property-can-be-null-or-a-reference-with-swagger
                    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://mcmap.net/q/145809/-how-to-specify-a-property-as-null-or-a-reference
                    //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:

Ruiz answered 18/5, 2023 at 15:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.