Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Missing OpenAPI spec nullable in classes with refs #2225

Closed
HavenDV opened this issue Sep 12, 2021 · 6 comments
Closed

[BUG] Missing OpenAPI spec nullable in classes with refs #2225

HavenDV opened this issue Sep 12, 2021 · 6 comments

Comments

@HavenDV
Copy link

HavenDV commented Sep 12, 2021

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
Copy link
Author

HavenDV commented Sep 12, 2021

About ValueTuple support:

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

produce:

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

in 6.2.1

@thisisthekap
Copy link

This one blocks us from using swashbuckle.

@liesahead
Copy link

liesahead commented Sep 23, 2022

Up

@miksh7
Copy link

miksh7 commented Feb 1, 2023

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
      }
    }
  }

@rgavrilov
Copy link

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.

@martincostello
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants