RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.78k stars 1.29k forks source link

different behavior for typemappings between net7 and net8 #4769

Open anotherthomas opened 9 months ago

anotherthomas commented 9 months ago

Hi, I have a dotnet backend and a typescript frontend. To get somewhat sensible date handling, nodatime types are mapped to temporal for the front end. There's a test controller to validate the behavior (stripped down to the problematic parts):

public class TemporalTestController : ControllerBase
{
    [HttpGet]
    public async Task<ActionResult<TemporalTestPayload>> Get([Required][FromQuery] LocalDate date)
    {
        return Ok(await Task.FromResult(TemporalTestPayload with { LocalDate = date }));
    }
}

public record TemporalTestPayload
{
    [Required]
    public LocalDate LocalDate { get; init; }
}

Running openapi generation with net7 results in the following snippet:

      "post": { 
        "tags": [ 
          "TemporalTest" 
        ], 
        "operationId": "TemporalTest_CreateFromForm", 
        "requestBody": { 
          "content": { 
            "multipart/form-data": { 
              "schema": { 
                "properties": { 
                  "LocalDate": { 
                    "type": "string", 
                    "format": "temporal-plain-date" 
                  } 
                } 
              } 
            } 
          } 
        }, 
        "responses":  
      }

running openapi generation with net8:

     "post": {
        "tags": [
          "TemporalTest"
        ],
        "operationId": "TemporalTest_CreateFromForm",
        "parameters": [ ],
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "properties": {
                  "LocalDate": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": 
      }   

So net8 is not picking up the typemappings in schemasettings.

Is that a bug in the code generation or a misunderstanding on my side?

xerus-github commented 7 months ago

(I am a co-worker of anotherthomas)

The essential hint is maybe that the behavior is buggy w.r.t. "multipart/form-data". (I.e., in the upper code example, there should be a [FromForm] attribute at a request parameter). Other occurrences, such response object or controller parameters, do work as before. So, in order to re-iterate (controller method):

    [HttpPost]
    public async Task<ActionResult<TestPayload>> Upload([Required][FromForm] TestPayload request)
    {
        return Ok(await Task.FromResult(request));
    }

with

public record TestPayload
{
    [Required]
    public LocalDate LocalDate { get; init; }

    [Required]
    public Foo Foo { get; init; }
}

public enum Foo
{
    VALUE1, VALUE2
}

This leads to a swagger.json with dotnet 8:

     ...
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "properties": {
                  "LocalDate": {
                    "type": "string",
                    "nullable": true
                  },
                  "Foo": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
     ...

Changing to dotnet 7 (but using the same NSwag version 14.0.3):

    ...
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "properties": {
                  "LocalDate": {
                    "type": "string",
                    "format": "temporal-plain-date"
                  },
                  "Foo": {
                    "$ref": "#/components/schemas/Foo"
                  }
                }
              }
            }
          }
        },
   ...

In both cases (dotnet 7 and 8), there is a schema definition block

"components": {
    "schemas": {
        "TestPayload": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "localDate",
              "foo"
            ],
            "properties": {
              "localDate": {
                "type": "string",
                "format": "temporal-plain-date",
                "minLength": 1
              },
              "foo": {
                "$ref": "#/components/schemas/Foo"
              }
            }
          },
          "Foo": {
            "type": "integer",
            "description": "",
            "x-enumNames": [
              "VALUE1",
              "VALUE2"
            ],
            "enum": [
              0,
              1
            ]
          },
       }
   ...

Interestingly, in dotnet 8 the response does work exactly as expected. This the reason why there is also this schema block above in dotnet 8. However, the request object in "multipart/form-data" does neither pick up a reference to the Foo enum nor does it use our custom formatter for "temporal-plain-date". It simply defaults to string in both cases and ignores the real schema.

(Let us ignore the fact for the moment that [Required] is also not parsed correctly in both cases (dotnet 7 and 8); probably, there is another issue for that.)

We would be very happy about any hints or ideas about this new form-data behavior.

dIeGoLi commented 7 months ago

I am having the same problem with enum values bein string/nullable instead of int when the controller parameter is [FromForm] /multipart/formdata instead of as body with application/json. The Problem is with the NSwag.AspNetCore package. The (default) swashbuckle generated open api swagger.json is correct in that aspect.

g0dm0d3 commented 7 months ago

We have the same issue, we used a workround with a custom IApiDescriptionGroupCollectionProvider. Hope this will be fixed soon