OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
20.73k stars 6.32k forks source link

[BUG][JAVA][SPRING] Endpoints don't support different schema per content-type #6708

Open bonii-xx opened 4 years ago

bonii-xx commented 4 years ago

Bug Report Checklist

Description

We want to support multiple schema for a single endpoint, depending on content-types. According to https://swagger.io/docs/specification/describing-request-body/ this is exactly what the OpenAPI spec should support:

The requestBody is more flexible in that it lets you consume different media types, such as JSON, XML, form data, plain text, and others, and use different schemas for different media types.

However the generator always combines the different content-types into a single endpoint, only using the first content-type, with the following output:

[INFO] --- openapi-generator-maven-plugin:4.3.1:generate (testEndpoint) @ pac-service ---
[INFO] OpenAPI Generator: spring (server)
[INFO] Generator 'spring' is considered stable.
[WARNING] Multiple schemas found in the OAS 'content' section, returning only the first one (application/x.foo+json)
Multiple schemas found in the OAS 'content' section, returning only the first one (application/x.foo+json)
openapi-generator version

openapi-generator-maven-plugin 4.3.1

OpenAPI declaration file content or url

https://gist.github.com/bonii-xx/1d48dd6d8e624aa3689cbb062ac2a7d2

Command line used for generation

Via openapi-generator-maven-plugin.

Steps to reproduce

Paste file above into https://editor.swagger.io/ Click on Generate Server -> spring Check code of generated FoobarApi Expect 2 endpoints with different RequestBodies, but only one is present handling both. The second request body schema for the second content type is not accepted.

Related issues/PRs

https://github.com/OpenAPITools/openapi-generator/issues/144 https://github.com/OpenAPITools/openapi-generator/issues/3990 https://github.com/OpenAPITools/openapi-generator/pull/3991

gbormann commented 3 years ago

Not just for requestBodies, also for reponse: http://spec.openapis.org/oas/v3.0.3#response-object

Staff-d commented 3 years ago

I'd like to push this :slightly_smiling_face:

elonderin commented 2 years ago

Not just for requestBodies, also for reponse: http://spec.openapis.org/oas/v3.0.3#response-object

jup we just ran into that too. IMO this is even the more urgent gap here b/c typically u might want to add some structured error responses for a 5xx (see also https://datatracker.ietf.org/doc/rfc7807/).

Moreover, when declaring diff. responses per code then u dont even get a warning.

Suggestion: can we adapt the title to "Endpoints don't support different schema per status code and/or content-type" Maybe there is some bug around for this, but if found none.

Question: Are there some workarounds available ?

gbormann commented 2 years ago

Sorry, can't remember what I did exactly on the project I needed it for (contract finished) but I believe it's a show stopper when sticking to a single endpoint. So, you'll have to either multiplex at the response object type level (with client-side demultiplexing, i.e. type-switching, Yuck!!) or define separate endpoints (again typr-switching at the client-side).

In other words you're forced to introduce a client-side design stink!!

If I remember correctly from looking at the source, the issue is hard to fix because there is a fundamental design decision at the heart of the issue based on a bad assumption about the spec, probably by reading too much into the example without properly reading the spec itself (the type of the 'content' field is quite explicitly a Map["String", Media Type Object]). That, or pointlessly assuming nobody would need it anyway.

API endpoints are not RPCs and hence are not uniquely valued mathematical functions!

At the very least, it should be mentioned under Limitations or Errata!

I haven't checked newer versions but if there is still no traction at all, this is yet another dead open source project. This is a fundamental implementation flaw, regardless of theoretical discussions about the necessity of multi-typed response bodies.

cdomigan commented 2 years ago

Oh man this is major bummer. Any other openapi generators that support this feature that you know of?

spacether commented 2 years ago

Heads up; our java layer was missing this needed request body content type information. I will add it in this PR: https://github.com/OpenAPITools/openapi-generator/pull/10973 After that lands any generator can use that information to include multiple content types in request bodies

Once it lands I will work on implementing it in python-experimental: https://github.com/OpenAPITools/openapi-generator/pull/8325

spacether commented 2 years ago

Not just for requestBodies, also for reponse...

@gbormann Response content and header parameters were also added in https://github.com/OpenAPITools/openapi-generator/pull/11046 so all generators now have this data available and can use it to support schema per body content type serialization and deserialization.

spacether commented 2 years ago

For python-experimental I am using #10973 in this PR: https://github.com/OpenAPITools/openapi-generator/pull/8325 to allow users to send request bodies with different body content types supported. One can see it working here: https://github.com/OpenAPITools/openapi-generator/blob/master/samples/openapi3/client/petstore/python-experimental/petstore_api/api/pet_api_endpoints/add_pet.py#L78

pet_parameter = api_client.RequestBody(
    content={
        'application/json': api_client.MediaType(schema=SchemaForRequestBodyApplicationJson),
        'application/xml': api_client.MediaType(schema=SchemaForRequestBodyApplicationXml),
    },
    required=True,
)

Java and other generators could do something similar

pedrograu commented 1 year ago

Another push more here. I found this issue when I tried to return two different response, each of them with a different content type, on the same endpoint. Following as it is described here

This yaml code

/pet/{petId}:
get:
  description: Get pet
  operationId: getPet
  tags:
    - REST
  parameters:
    - in: path
      name: petId
      schema:
        type: string
        format: uuid
      required: true
  responses:
    200:
      description: Returns PDF
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/pet'
        application/pdf:
          schema:
            type: string
            format: binary
      headers:
        Content-Disposition:
          schema:
            type: string
            description: Used only with `application/pdf` responses
            example: attachment; filename="name.pdf"
    '404':
      description: Pet not found for the petId
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/error-response'

Generate the following code:

/**
     * GET /pet/{petId}
     * Get pet
     *
     * @param petId  (required)
     * @return Returns PDF (status code 200)
     *         or Pet not found for the petId (status code 404)
     */
@Operation(
    operationId = "getPet",
    tags = { "REST" },
    responses = {
        @ApiResponse(responseCode = "200", description = "Returns PDF", content = {
            @Content(mediaType = "application/json", schema = @Schema(implementation = PetDTO.class)),
            @Content(mediaType = "application/pdf", schema = @Schema(implementation = PetDTO.class))
        }),
        @ApiResponse(responseCode = "404", description = "Pet not found for the petId", content = {
            @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponseDTO.class)),
            @Content(mediaType = "application/pdf", schema = @Schema(implementation = ErrorResponseDTO.class))
        })
    },
    security = {
        @SecurityRequirement(name = "bearer-key")
    }
)
@RequestMapping(
        method = RequestMethod.GET,
        value = "/pet/{petId}",
        produces = { "application/json", "application/pdf" }
    )
    default ResponseEntity<PetDTO> getPet(
        @Parameter(name = "petId", description = "", required = true) @PathVariable("petId") UUID petId
    ) {
        getRequest().ifPresent(request -> {
            for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
                if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                    String exampleString = "{ \"name\" : \"Toby\",  \"id\" : \"046b6c7f-0b8a-43b9-b35d-6489e6daee91\" }";
                    ApiUtil.setExampleResponse(request, "application/json", exampleString);
                    break;
                }
                if (mediaType.isCompatibleWith(MediaType.valueOf("application/pdf"))) {
                    String exampleString = "Custom MIME type example not yet supported: application/pdf";
                    ApiUtil.setExampleResponse(request, "application/pdf", exampleString);
                    break;
                }
            }
        });
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

Due the method on the interface generated is defined to return a ResponseEntity<PetDTO>, you cannot implement a method that return something else depending of the Accept header.

Anonymous-Coward commented 6 months ago

This should be addressed with somewhat higher prio - it's opened almost four years ago, and is a showstopper in many cases. The example @pedrograu provided above perfectly illustrates the problem. Unless you under-specify your API endpoints, i.e. leave out the schema for the returned values and just specify them as object, you can only return one single type for all mime types. This is obviously not convenient. A simple solution would be to simply generate handler methods to return ResponseEntity<?> when there are multiple return types possible, maybe controlled by a config parameter for the generator.

asathyanarayan commented 4 months ago

How is this an open issue for 4 years ? that means there is a work around for this ? would love to know . The solution suggested by @Anonymous-Coward seems like a viable option 🤔