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
21.87k stars 6.59k forks source link

[REQ] Media type versioning #3569

Open ferblaca opened 5 years ago

ferblaca commented 5 years ago

Problem

It is not possible to use the Accept Header in the request for versioning endpoint rest services (instead of URI Versioning).

by example, We want versioning the "findStock" method using the content-type for the same path "/stock" in this contract first:

  /stock:
    get:
      tags:
        - stock
      summary: Get stock
      operationId: findStock
      responses:
        '200':
          description: A paged array of Inventory Items
          content:
            "application/vnd.mecstkac.v1.0.0+json":
              schema:
                $ref: "#/components/schemas/InventoryItemV1"
            "application/vnd.mecstkac.v1.0.1+json":
              schema:
                $ref: "#/components/schemas/InventoryItemV2"

When we generate the code from the contract described above, it generates an API with a single "findStock" method that returns only one type of response:

    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "A paged array of Inventory Items", response = InventoryItemV1.class) })
    @RequestMapping(value = "/stock",
        produces = { "application/vnd.mecstkac.v1.0.0+json", "application/vnd.mecstkac.v1.0.1+json" }, 
        method = RequestMethod.GET)
    default ResponseEntity<InventoryItemV1> findStock() {
        getRequest().ifPresent(request -> {
            for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
                if (mediaType.isCompatibleWith(MediaType.valueOf(""))) {
                    ApiUtil.setExampleResponse(request, "", "");
                    break;
                }
            }
        });
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

Solution

Use the Accept Header in the request for versioning endpoint rest. By example, We want versioning the "findStock" method using the content-type for the same path "/stock" in this contract first:

  /stock:
    get:
      tags:
        - stock
      summary: Get stock
      operationId: findStock
      responses:
        '200':
          description: A paged array of Inventory Items
          content:
            "application/vnd.mecstkac.v1.0.0+json":
              schema:
                $ref: "#/components/schemas/InventoryItemV1"
            "application/vnd.mecstkac.v1.0.1+json":
              schema:
                $ref: "#/components/schemas/InventoryItemV2"

-> if content-type is "application/vnd.mecstkac.v1.0.0+json" return response "InventoryItemV1" -> if content-type is "application/vnd.mecstkac.v1.0.1+json" return response "InventoryItemV2"

the self-generated code for server interface would be something like :

  @GetMapping(value = "/stock", produces = "application/vnd.mecstkac.v1.0.0+json")
  public ResponseEntity<InventoryItemV1> findStockV1_0_0() {
    getRequest().ifPresent(request -> {
        for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
            if (mediaType.isCompatibleWith(MediaType.valueOf(""))) {
                ApiUtil.setExampleResponse(request, "", "");
                break;
            }
        }
    });
    return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
  }

  @GetMapping(value = "/stock", produces = "application/vnd.mecstkac.v1.0.0+json")
  public ResponseEntity<InventoryItemV2> findStockV1_0_1() {
    getRequest().ifPresent(request -> {
        for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
            if (mediaType.isCompatibleWith(MediaType.valueOf(""))) {
                ApiUtil.setExampleResponse(request, "", "");
                break;
            }
        }
    });
    return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
  }

the self-generated code for client interface would be something like :

    public InventoryItemV1 findStockV1_0_0() throws RestClientException {
        Object postBody = null;

        String path = UriComponentsBuilder.fromPath("/stock").build().toUriString();

        final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<String, String>();
        final HttpHeaders headerParams = new HttpHeaders();
        final MultiValueMap<String, Object> formParams = new LinkedMultiValueMap<String, Object>();

        final String[] accepts = { "application/vnd.mecstkac.v1.0.0+json" };
        final List<MediaType> accept = apiClient.selectHeaderAccept(accepts);
        final String[] contentTypes = { };
        final MediaType contentType = apiClient.selectHeaderContentType(contentTypes);

        String[] authNames = new String[] {  };

        ParameterizedTypeReference<InventoryItemV1> returnType = new ParameterizedTypeReference<InventoryItemV1>() {};
        return apiClient.invokeAPI(path, HttpMethod.GET, queryParams, postBody, headerParams, formParams, accept, contentType, authNames, returnType);
    }

    public InventoryItemV2 findStockV1_0_1() throws RestClientException {
        Object postBody = null;

        String path = UriComponentsBuilder.fromPath("/stock").build().toUriString();

        final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<String, String>();
        final HttpHeaders headerParams = new HttpHeaders();
        final MultiValueMap<String, Object> formParams = new LinkedMultiValueMap<String, Object>();

        final String[] accepts = { "application/vnd.mecstkac.v1.0.1+json" };
        final List<MediaType> accept = apiClient.selectHeaderAccept(accepts);
        final String[] contentTypes = { };
        final MediaType contentType = apiClient.selectHeaderContentType(contentTypes);

        String[] authNames = new String[] {  };

        ParameterizedTypeReference<InventoryItemV1> returnType = new ParameterizedTypeReference<InventoryItemV1>() {};
        return apiClient.invokeAPI(path, HttpMethod.GET, queryParams, postBody, headerParams, formParams, accept, contentType, authNames, returnType);
    }

for both reactive and non-reactive implementation.

stahloss commented 4 years ago

Problem is that you can have multiple media types per endpoint anyway, and you do not always want different handlers per media type. So preferably, this should be addressed at a higher level (path:method). This way you can define different operations for the same path. See https://github.com/OAI/OpenAPI-Specification/issues/2142#issue-565820317

shivaprasadgurram commented 2 years ago

@ferblaca does you found a way to achieve as per your expectation? I'm also looking for a solution to this problem. Please help me if you know solution for this problem.

stefchel commented 1 year ago

I might have found a workaround for this issue. Just define your v2 in a separate file.

  /stock:
    get:
      tags:
        - stock
      summary: Get stock
      operationId: findStock
      responses:
        '200':
          description: A paged array of Inventory Items
          content:
            "application/vnd.mecstkac.v1.0.0+json":
              schema:
                $ref: "#/components/schemas/InventoryItemV1"
  /stock:
    get:
      tags:
        - stock
      summary: Get stock
      operationId: findStockV2
      responses:
        '200':
          description: A paged array of Inventory Items
          content:
            "application/vnd.mecstkac.v1.0.1+json":
              schema:
                $ref: "#/components/schemas/InventoryItemV2"

Using the maven plugin, I am able to generate two Interfaces (one for each version) by adding a second <execution> block for v2 of the spec. Just make sure the API for v2 doesn't override the generate v1 API (unfortunately the apiNameSuffix configuration doesn't work for spring: https://github.com/OpenAPITools/openapi-generator/issues/8822). Your Controller can then implement both interfaces. Of course, this only works if you're able to split your API specification into two files, which might not always be desirable (hence I consider this just a workaround).

bushwakko commented 7 months ago

Problem is that you can have multiple media types per endpoint anyway, and you do not always want different handlers per media type. So preferably, this should be addressed at a higher level (path:method). This way you can define different operations for the same path. See OAI/OpenAPI-Specification#2142 (comment)

It could create a new method only when the return-type differs? Right now if there are mutually exclusive return types, it will just choose the first one, while still claiming to support all media-types.