RicoSuter / NSwag

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

Schema definition is wrong: Custom exception's properties are not camel case #3072

Open aureole82 opened 4 years ago

aureole82 commented 4 years ago

When I create a very simple Swagger API:

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddControllers()
        .AddNewtonsoftJson(options =>
        {
            options.SerializerSettings.Converters.Add(new StringEnumConverter());
            // We only allow undefined in TS. So ignore null properties.
            options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
        })
        ;

    // Register the Swagger services.
    services.AddSwaggerDocument();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    // Register the Swagger generator and the Swagger UI middleware.
    app.UseOpenApi();
    app.UseSwaggerUi3();

    ...
}

with this Controller Action:

[HttpGet("city/{city}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OtherException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(CityNotFoundException), StatusCodes.Status404NotFound)]
public async Task<ActionResult<WeatherForecast[]>> Get(string city)
{
    try
    {
        return Ok(await _Get(city));
    }
    catch (CityNotFoundException e)
    {
        return NotFound(e);
    }
    catch (OtherException e)
    {
        return BadRequest(e);
    }
}

the "definitions" schema looks like:

{
  "WeatherForecast": {
    "type": "object",
    "required": [
      "date",
      "temperatureC",
      "temperatureF"
    ],
    "properties": {
      "city": {
        "type": "string"
      },
      "date": {
        "type": "string",
        "format": "date-time"
      },
      "temperatureC": {
        "type": "integer",
        "format": "int32"
      },
      "temperatureF": {
        "type": "integer",
        "format": "int32"
      },
      "summary": {
        "type": "string"
      }
    }
  },
  "OtherException": {
    "allOf": [
      {
        "$ref": "#/definitions/Exception"
      },
      {
        "type": "object",
        "required": [
          "timestamp"
        ],
        "properties": {
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    ]
  },
  "Exception": {
    "type": "object",
    "required": [
      "Message"
    ],
    "properties": {
      "StackTrace": {
        "type": "string"
      },
      "Message": {
        "type": "string"
      },
      "InnerException": {
        "$ref": "#/definitions/Exception"
      },
      "Source": {
        "type": "string"
      }
    }
  },
  "CityNotFoundException": {
    "allOf": [
      {
        "$ref": "#/definitions/Exception"
      },
      {
        "type": "object"
      }
    ]
  }
}

You'll notice the properties of base Exception are all pascal case. That's wrong because the response will be camel case:

{
    "timestamp": "2020-09-24T15:50:55.1297797+02:00",
    "stackTrace": "   at WebApplication1.Controllers.WeatherForecastController._Get(String city) in C:\\Users\\xxx\\Projects\\WebApplication1\\WebApplication1\\Controllers\\WeatherForecastController.cs:line 50\r\n   at WebApplication1.Controllers.WeatherForecastController.Get(String city) in C:\\Users\\xxx\\Projects\\WebApplication1\\WebApplication1\\Controllers\\WeatherForecastController.cs:line 34",
    "message": "City required: -",
    "data": {},
    "source": "WebApplication1",
    "hResult": -2146233088
}

This would result in buggy generated clients, e.g. Typescript:

export class Exception implements IException {
    ...

    init(_data?: any) {
        if (_data) {
            this.stackTrace = _data["StackTrace"]; // FAIL!
            this.message = _data["Message"]; // FAIL!
            this.innerException = _data["InnerException"] ? Exception.fromJS(_data["InnerException"]) : <any>undefined;  // FAIL!
            this.source = _data["Source"]; // FAIL!
        }
    }

    ...
}

WebApplication1.zip

RicoSuter commented 4 years ago

I think you need this: https://github.com/RicoSuter/NSwag/wiki/JsonExceptionFilterAttribute