Swashbuckle.AspNetCore icon indicating copy to clipboard operation
Swashbuckle.AspNetCore copied to clipboard

[BUG] Missing OpenAPI spec nullable in classes with refs

Open HavenDV opened this issue 3 years ago • 3 comments

I have generated code:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Dedoose.Apis.Servers.Controllers.Core;
using Dedoose.Apis.Servers.Services;
using Dedoose.Apis.Services;
using Dedoose.Core.Data.Model;
using Dedoose.Services.DTO;
using Dedoose.Services.Services.DTO;
using Dedoose.Data.DTO;
using Dedoose.Core.Data.DTO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Swashbuckle.AspNetCore.Annotations;

#pragma warning disable CA1054
#pragma warning disable CS8604
#pragma warning disable CS8620
#pragma warning disable CS0618

#nullable enable

namespace Dedoose.Apis.Servers.Controllers.Generated
{
    [ApiController]
    [Route("api/v1/user/addnewusertoaccount")]
    [Produces("application/json")]
    public class UserService_AddNewUserToAccountController : ConnectionController<UserService_AddNewUserToAccountController>
    {
        #region Constructors

        public UserService_AddNewUserToAccountController(
            ILogger<UserService_AddNewUserToAccountController> logger, 
            ConnectionService connectionService) :
            base(logger, connectionService)
        {
        }

        #endregion

        #region Methods

        /// <summary>
        /// AddNewUserToAccount.
        /// </summary>
        /// <param name="token">Access token.</param>
        /// <param name="projectId">projectId.</param>
        /// <param name="username">username.</param>
        /// <param name="firstName">firstName.</param>
        /// <param name="lastName">lastName.</param>
        /// <param name="email">email.</param>
        /// <param name="phone">phone.</param>
        /// <param name="encryptedPass">encryptedPass.</param>
        /// <param name="groupId">groupId.</param>
        /// <param name="cancellationToken"></param>
        /// <returns>AddNewUserToAccount.</returns>
        /// <response code="200">Successful request.</response>
        /// <response code="400">Token is null.</response>    
        /// <response code="401">Token is invalid.</response>   
        [HttpGet]
        [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserService_AddNewUserToAccountControllerOutput))]
        [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(string))]
        [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(string))]
        [SwaggerOperation(Tags = new[] { "User" }, OperationId = "UserAddNewUserToAccount")]
        public Task<IActionResult> GetAsync(
            [FromHeader, SwaggerParameter("Access Token.")] string token,
            [FromQuery] Guid projectId,
            [FromQuery] String? username,
            [FromQuery] String? firstName,
            [FromQuery] String? lastName,
            [FromQuery] String? email,
            [FromQuery] String? phone,
            [FromQuery] String? encryptedPass,
            [FromQuery] Nullable<Guid> groupId,
            CancellationToken cancellationToken = default)
        {
            return RunAsync(
                token,
                async connection => (UserService_AddNewUserToAccountControllerOutput)await connection.Client.CallAsync(
                    UserService.AddNewUserToAccount.Call((projectId, username, firstName, lastName, email, phone, encryptedPass, groupId)), cancellationToken).ConfigureAwait(false));
        }

        #endregion
    }

    public class UserService_AddNewUserToAccountControllerOutput
    {
        public User? UserData { get; set; }
        public UserGroupLink? UserGroupLinkData { get; set; }

        public static explicit operator UserService_AddNewUserToAccountControllerOutput(
            (
            User? userData,
            UserGroupLink? userGroupLinkData
            ) tuple)
        {
            return new UserService_AddNewUserToAccountControllerOutput
            {
                UserData = tuple.userData,
                UserGroupLinkData = tuple.userGroupLinkData,
            };
        }
    }
}

It produces this json open API spec for UserService_AddNewUserToAccountControllerOutput:

      "UserService_AddNewUserToAccountControllerOutput": {
        "type": "object",
        "properties": {
          "userData": {
            "$ref": "#/components/schemas/User"
          },
          "userGroupLinkData": {
            "$ref": "#/components/schemas/UserGroupLink"
          }
        },
        "additionalProperties": false
      },

It should produce this json open API spec:

"UserService_AddNewUserToAccountControllerOutput": {
        "type": "object",
        "properties": {
          "userData": {
            "$ref": "#/components/schemas/User",
            "nullable": true
          },
          "userGroupLinkData": {
            "$ref": "#/components/schemas/UserGroupLink",
            "nullable": true
          }
        },
        "additionalProperties": false
      },

Versions:

<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.2.1" />

Also, you might want to add support for ValueTuple. I was not able to use it directly, so I generate an additional class with an explicit operator like here. I tested this on version 6.1.4

HavenDV avatar Sep 12 '21 10:09 HavenDV

About ValueTuple support:

[ProducesResponseType(StatusCodes.Status200OK, Type = typeof((User? userData, UserGroupLink? userGroupLinkData)))]

produce:

"UserUserGroupLinkValueTuple": {
        "type": "object",
        "additionalProperties": false
      },

in 6.2.1

HavenDV avatar Sep 12 '21 10:09 HavenDV

This one blocks us from using swashbuckle.

thisisthekap avatar Feb 09 '22 10:02 thisisthekap

Up

liesahead avatar Sep 23 '22 07:09 liesahead

Same issue with nullable properties. How to force #ref to be nullable in code?

    public class MyClass
    {
        // This must be nullable in api json but it's not!!
        public RefClass? NullableObject { get; set; }
        // Non-nullable list is marked as nullable!!
        public List<RefClass> NonNullableList { get; set; }
        // Nullable List is marked as nullable - ok
        public List<RefClass>? NullableListList { get; set; }
    }

    public class RefClass
    {
        public int MyProperty { get; set; }
    }

ver 6.5.0 generates

  "components": {
    "schemas": {
      "MyClass": {
        "type": "object",
        "properties": {
          "nullableObject": {
            "$ref": "#/components/schemas/RefClass"
          },
          "nonNullableList": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RefClass"
            },
            "nullable": true
          },
          "nullableListList": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RefClass"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "RefClass": {
        "type": "object",
        "properties": {
          "myProperty": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false
      }
    }
  }

miksh7 avatar Feb 01 '23 19:02 miksh7

Is there any movement on this? I can hack around it by providing a schema filter, but not having runtime type of the properties inside ISchemaFilter makes it hacky:

public class NullableSchemaFilter : ISchemaFilter {
    public void Apply(OpenApiSchema schema, SchemaFilterContext context) {
        if (schema.Type == "object") {
            var allProps = context.Type.GetProperties();

            foreach (var openApiSchema in schema.Properties) {
                // Note: this is a bit hacky.
                // I don't see a way to get to the runtime type from property name.
                // so I am assuming that our properties are not going to differ by casing only.
                var runtimeTypeProp = allProps.FirstOrDefault(p =>
                    string.Equals(p.Name, openApiSchema.Key, StringComparison.OrdinalIgnoreCase));
                var isNullable = runtimeTypeProp is not null && NullableHelperClass.IsNullable(runtimeTypeProp);

                if (!isNullable && openApiSchema.Value.Nullable == false) {
                    schema.Required.Add(openApiSchema.Key);
                }
            }
        }
    }
}

It can be handled better using _generatorOptions.CustomTypeMappings but then you lose all the data generation benefits of the SchemaGenerator.

rgavrilov avatar Mar 07 '23 14:03 rgavrilov

To make issue tracking a bit less overwhelming for the new maintainers (see #2778), I've created a new tracking issue to roll-up various nullability issues here: #2793.

We'll refer back to this issue from there and include it as part of resolving that issue, but I'm going to close this one to help prune the backlog.

martincostello avatar Apr 14 '24 10:04 martincostello