RicoSuter / NSwag

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

Default swagger content-type for Response #1566

Open OculiViridi opened 6 years ago

OculiViridi commented 6 years ago

Since latest 2-3 releases (I don't know exactly which one) I notice that the default content-type selected on the swagger HTML dropdown menu for the method reponse, is not "application/json" but "text/plain".

If I don't change it everytime before clicking on Try button, I get an error because no content-type negotiation for responses is allowed in my application. I think that "application/json" should be the right default value, but anyway is it possible to set the default content-type for response in Swagger configuration to avoid changing it everytime?

This is my actual configuration

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(config =>
    {
        // HTTP 406 when not supported format is requested by client
        config.ReturnHttpNotAcceptable = true;
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
    // Add FluentValidation
    .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<MachineCreateModelValidator>());

    // Add API versioning
    services.AddApiVersioning(options =>
    {
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1, 0);
        options.ReportApiVersions = true;
    });
}

The HTML result

nswag content-type

RicoSuter commented 6 years ago

I just tried with the latest version (v18.19.0) and it is set to json by default (it only has json):

image

Can you provide a sample and/or provide the controller code?

RicoSuter commented 6 years ago

Can you also show your NSwag middleware registration in startup.cs?

OculiViridi commented 6 years ago

@RSuter I've already updated to v11.19.0.

Here it is my Startup.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(config =>
            {
                // HTTP 406 when not supported format is requested by client
                config.ReturnHttpNotAcceptable = true;
            })
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
            // Add FluentValidation
            .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<MachineCreateModelValidator>());

        // Add API versioning
        services.AddApiVersioning(options =>
            {
                options.AssumeDefaultVersionWhenUnspecified = true;
                options.DefaultApiVersion = new ApiVersion(1, 0);
                options.ReportApiVersions = true;
            });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseHsts();
            // Add Exception handling
            app.UseGlobalExceptionHandler();
        }

        // Add Swagger
        app.UseSwagger();

        app.UseMvc();
    }
}

UseSwagger extension method

public static class ServiceExtensions
{
    public static void UseSwagger(this IApplicationBuilder app)
    {
        string title = "My Control API";
        string description = "My API for Core functionalities.";

        app.UseSwaggerWithApiExplorer(config =>
        {
            config.GeneratorSettings.OperationProcessors.TryGet<ApiVersionProcessor>().IncludedVersions = new[] { "1.0" };
            config.SwaggerRoute = "v1.0.json";

            config.GeneratorSettings.Title = title;
            config.GeneratorSettings.Description = description;
        });

        app.UseSwaggerWithApiExplorer(config =>
        {
            config.GeneratorSettings.OperationProcessors.TryGet<ApiVersionProcessor>().IncludedVersions = new[] { "2.0" };
            config.SwaggerRoute = "v2.0.json";

            config.GeneratorSettings.Title = title;
            config.GeneratorSettings.Description = description;
        });

        app.UseSwaggerUi3(config =>
        {
            config.SwaggerRoutes.Add(new SwaggerUi3Route("v1.0", "/v1.0.json"));
            config.SwaggerRoutes.Add(new SwaggerUi3Route("v2.0", "/v2.0.json"));
        });
    }
}

And this is one of the controllers (Machines)

[ApiVersion("1.0")]
[SwaggerTag("Machines", Description = "Core operations on machines.")]
public class MachinesController : BaseController
{
    [HttpGet("{id:long}")]
    [ProducesResponseType((int)HttpStatusCode.OK)]
    public async Task<ActionResult<Machine>> Get(long id)
    {
        return await Mediator.Send(new GetMachineByIdQuery() { Id = id });
    }

    [HttpGet]
    [ProducesResponseType((int)HttpStatusCode.OK)]
    public async Task<ActionResult<List<Machine>>> List()
    {
        return await Mediator.Send(new GetMachineListQuery());
    }

    [HttpPost]
    [ProducesResponseType((int)HttpStatusCode.Created)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    public async Task<IActionResult> Create(MachineCreateModel machine)
    {
        if (machine == null)
            return BadRequest();

        long newId = await Core.CreateMachine(machine);

        return CreatedAtAction(nameof(Get), new { id = newId }, null);
    }

    [HttpPut]
    [ProducesResponseType((int)HttpStatusCode.Accepted)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    public async Task<IActionResult> Update(long id, Machine machine)
    {
        if (machine == null || machine.Id != id)
        {
            return BadRequest();
        }

        await Mediator.Send(new MachineUpdateCommand { Machine = machine });

        return AcceptedAtAction(nameof(Get), new { id = id }, machine);
    }

    [HttpPost]
    public async Task<IActionResult> NormalizeConfiguration(int machineId)
    {
        await Core.MachineNormalizeConfiguration(machineId);

        return Ok();
    }

    [HttpPost]
    public async Task<IActionResult> Start(int machineId)
    {
        await Core.MachineStart(machineId);

        return Ok();
    }

    [HttpPost]
    public async Task<IActionResult> Stop(int machineId)
    {
        await Core.MachineStop(machineId);

        return Ok();
    }
}
RicoSuter commented 6 years ago

I'd say UseSwagger() is Swashbuckle and not NSwag, so you need to check in the Swashbuckle docs why this happens, right?

OculiViridi commented 6 years ago

@RSuter Sorry! My fault! I was missing the code of MY UseSwagger() extension method!!! 😛 I've updated the previous post. Please take a look now... Thanks!

OculiViridi commented 5 years ago

@RSuter I've just updated to latest version (11.19.2) but the problem still remains. What am I doing wrong?

I've noticed just now, that the default content-type selected on the dropdowns is different for GET and POST/PUT. So for GETs there's "text/plain" and for POSTs/PUTs "application/json" instead.

OculiViridi commented 5 years ago

@RSuter Hi! I'm now on v11.20.1, but the problem is still there. As described in my last comment

the default content-type selected on the dropdowns is different for GET and POST/PUT. So for GETs there's "text/plain" and for POSTs/PUTs "application/json" instead

2018-10-23 17_42_15-swagger ui

2018-10-23 17_45_31-swagger ui

2018-10-23 17_46_02-swagger ui

I think it can be a matter of configuration... can we investigate further?

RicoSuter commented 5 years ago

Did you check the generated swagger.json? There you will see text/plain somewhere.. can you post that?

OculiViridi commented 5 years ago

Here it is my JSON. There's "text/plain" as first value of produces list in the /api/Machines/Get method.

{
  "x-generator": "NSwag v11.20.1.0 (NJsonSchema v9.11.0.0 (Newtonsoft.Json v11.0.0.0))",
  "swagger": "2.0",
  "info": {
    "title": "Control API",
    "description": "API for Core functionalities.",
    "version": "1.0.0"
  },
  "host": "localhost:5001",
  "schemes": [
    "http"
  ],
  "consumes": [
    "application/json-patch+json",
    "application/json",
    "text/json",
    "application/*+json"
  ],
  "paths": {
    "/api/v1.0/Machines/Get/{id}": {
      "get": {
        "tags": [
          "Machines"
        ],
        "operationId": "Machines_Get",
        "produces": [
          "text/plain",
          "application/json",
          "text/json"
        ],
        "parameters": [
          {
            "type": "integer",
            "name": "id",
            "in": "path",
            "required": true,
            "format": "int64",
            "x-nullable": false
          }
        ],
        "responses": {
          "200": {
            "x-nullable": true,
            "description": "",
            "schema": {
              "$ref": "#/definitions/Machine"
            }
          }
        }
      }
    },
    "/api/v1.0/Machines/Create": {
      "post": {
        "tags": [
          "Machines"
        ],
        "operationId": "Machines_Create",
        "consumes": [
          "application/json-patch+json",
          "application/json",
          "text/json",
          "application/*+json"
        ],
        "parameters": [
          {
            "name": "machine",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/MachineCreateModel"
            },
            "x-nullable": false
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "400": {
            "description": ""
          }
        }
      }
    },
    "/api/v1.0/Machines/Update": {
      "put": {
        "tags": [
          "Machines"
        ],
        "operationId": "Machines_Update",
        "consumes": [
          "application/json-patch+json",
          "application/json",
          "text/json",
          "application/*+json"
        ],
        "parameters": [
          {
            "type": "integer",
            "name": "id",
            "in": "query",
            "format": "int64",
            "x-nullable": false
          },
          {
            "name": "machine",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/Machine"
            },
            "x-nullable": false
          }
        ],
        "responses": {
          "202": {
            "description": ""
          },
          "400": {
            "description": ""
          }
        }
      }
    }
  },
  "definitions": {
    "Machine": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "status",
        "machineDriverId"
      ],
      "properties": {
        "id": {
          "type": "integer",
          "format": "int64"
        },
        "name": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "mnmConfiguration": {
          "type": "string"
        },
        "status": {
          "$ref": "#/definitions/MachineBootStatus"
        },
        "machineDriverId": {
          "type": "integer",
          "format": "int64"
        },
        "machineDriver": {
          "$ref": "#/definitions/MachineDriver"
        },
        "driverConfiguration": {
          "type": "string"
        },
        "driverStatus": {
          "type": "string"
        }
      }
    },
    "MachineBootStatus": {
      "type": "string",
      "description": "",
      "x-enumNames": [
        "Disabled",
        "Stopped",
        "Started",
        "DriverNotFound",
        "InvalidDriverConfig",
        "InvalidMnmConfig",
        "NoRoutes",
        "Failed"
      ],
      "enum": [
        "Disabled",
        "Stopped",
        "Started",
        "DriverNotFound",
        "InvalidDriverConfig",
        "InvalidMnmConfig",
        "NoRoutes",
        "Failed"
      ]
    },
    "MachineDriver": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id"
      ],
      "properties": {
        "id": {
          "type": "integer",
          "format": "int64"
        },
        "model": {
          "type": "string"
        },
        "signature": {
          "type": "string"
        }
      }
    },
    "MachineCreateModel": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "machineDriverId"
      ],
      "properties": {
        "name": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "mnmConfiguration": {
          "type": "string"
        },
        "phaseName": {
          "type": "string"
        },
        "machineDriverId": {
          "type": "integer",
          "format": "int64"
        }
      }
    }
  },
  "tags": [
    {
      "name": "Machines",
      "description": "Core operations on machines."
    }
  ]
}
OculiViridi commented 5 years ago

I temporary solved the problem by simply putting the Produces attribute on the top of all of my controllers, like this:

[Produces("application/json")]
[ApiVersion("1.0")]
[SwaggerTag("Machines", Description = "Core operations on machines.")]
public class MachinesController : ControllerBase
{ }

UPDATE 2019/07/23

I forgot to mention that if you are using a common base class for all your Controllers, you can just put the Produces attribute only on the base class.

[ApiController]
[Produces("application/json")]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
public abstract class BaseController : ControllerBase
{
    // TODO: Add needed common methods/properties for all Controllers
}

[Authorize]
[ApiVersion("1.0")]
[OpenApiTag("Machines", Description = "Operations on machines")]
public class MachinesController : BaseController
{
    // TODO: Add controller methods
}

[Authorize]
[ApiVersion("1.0")]
[OpenApiTag("Machines", Description = "Operations on machines")]
public class DriversController : BaseController
{
    // TODO: Add controller methods
}

@RSuter But I don't know if it is really necessary, since you said that should come automatically. Any news?

su-rabbit commented 5 years ago

Also There are one more temporary resolving way.

Just override Produces on AddSwaggerDocument when add swagger document on startup.cs

services.AddSwaggerDocument(settings =>
{
    settings.PostProcess = document => document.Produces = new List<string>
    {
        "application/json",
        "text/json"
    };
});
OculiViridi commented 5 years ago

Also There are one more temporary resolving way.

Just override Produces on AddSwaggerDocument when add swagger document on startup.cs

services.AddSwaggerDocument(settings =>
{
  settings.PostProcess = document => document.Produces = new List<string>
  {
      "application/json",
      "text/json"
  };
});

@js-lee0624

Just a note for users reading the suggestion, from NSwag v13, there was a refactoring, so the method is called AddOpenApiDocument instead of AddSwaggerDocument.

tothdavid commented 5 years ago

@OculiViridi

you can just put the Produces attribute only on the base class.

I don't think that would be a good idea in many cases as Produces changes the behavior of the application, basically it turns off content negotiation. It might work if you always produce JSON output, but it is definitely not RESTful to ignore the Accept HTTP request header.

svisystem commented 3 years ago

Also There are one more temporary resolving way.

Just override Produces on AddSwaggerDocument when add swagger document on startup.cs

services.AddSwaggerDocument(settings =>
{
  settings.PostProcess = document => document.Produces = new List<string>
  {
      "application/json",
      "text/json"
  };
});

Just found this thread, but I unable to handle this issue with the solution provided above. We customize several NSwag related things, so I also tried on an NSwag sample project for .Net Core 3.1 with AddOpenApiDocument extension, but no luck.

MarcoMartins86 commented 4 months ago

I'm also having the same problem. For example, if I try to use something like this to define the output content type per status code

[ProducesResponseType(typeof(MyObj), StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest, MediaTypeNames.Text.Plain)]

The content type just gets ignored.

While debugging, I could see that the information was there but it was not used (OperationResponseProcessor.cs) image

The value comes from a hard-coded logic in the code (OpenApiResponse.cs) image

As a workaround, I did an Operation Processor to change the content type.

public class ForceResponseContentTypeAttribute(int statusCode, string contentType)
    : OpenApiOperationProcessorAttribute(typeof(ForceResponseContentTypeOperationProcessor), statusCode, contentType)
{
    private class ForceResponseContentTypeOperationProcessor(int statusCode, string contentType) : IOperationProcessor
    {
        public bool Process(OperationProcessorContext context)
        {
            var statusCodeString = statusCode.ToString();
            foreach (var response in context.OperationDescription.Operation.Responses)
            {
                if (response.Key == statusCodeString)
                { 
                    if (response.Value.Content is { Count: >= 1 })
                    {
                        var existentContent = response.Value.Content.First();
                        response.Value.Content.Remove(existentContent.Key);
                        response.Value.Content.Add(contentType, existentContent.Value);
                    }
                }
            }

            return true;
        }
    }
}

And use it like this

[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest, MediaTypeNames.Text.Plain)] // this prefills the needed data
[ForceResponseContentType(StatusCodes.Status400BadRequest, MediaTypeNames.Text.Plain)]

It won't work for all use cases but is enough for me.