Swashbuckle.AspNetCore
Swashbuckle.AspNetCore copied to clipboard
[BUG] Missing OpenAPI spec nullable in classes with refs
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
About ValueTuple support:
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof((User? userData, UserGroupLink? userGroupLinkData)))]
produce:
"UserUserGroupLinkValueTuple": {
"type": "object",
"additionalProperties": false
},
in 6.2.1
This one blocks us from using swashbuckle.
Up
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
}
}
}
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.
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.