OpenAPI-Specification icon indicating copy to clipboard operation
OpenAPI-Specification copied to clipboard

Allow versioning at path:method level

Open damonsutherland opened this issue 5 years ago • 8 comments

To facilitate the option of media-type versioning, it would be helpful to version at the path:method level. Here is the gist of my proposal: move version from root level to path:method level and represent as an array of versions. In making the change as described, my hope is that none of the existing functionality is sacrificed.

Single version example

/pets/{id}:
  put:
    tags:
    - "pets"
    versions:
    - version: "default"
      summary: "..."
      description: "..."
      ... all other properties formerly defined in the path-item

Multiple version example

/pets/{id}:
  put:
    tags:
    - "pets"
    versions:
    - version: "1"
      summary: "..."
      description: "..."
      operationId: "updatePet_v1"
      consumes:
      - "application/json"
      - "application/vnd.vendor.v1+json"
      deprecated: true
      ... all other properties currently defined in the path-item
    - version: "2"
      summary: "..."
      description: "..."
      operationId: "updatePet_v2"
      consumes:
      - "application/vnd.vendor.v2+json"
      ... all other properties currently defined in the path-item

damonsutherland avatar Feb 15 '20 22:02 damonsutherland

Your use of consumes leads me to think you are using / suggesting a change to OAS 2.0.

The 2.0 specification is no longer being worked on (for nearly 3 years), and OAS 3.0 should address your use-case for media-type versioning of request and response bodies.

Converters are available to ease the transition to 3.0.

If this answers your question, please could you close this issue, thanks.

MikeRalphson avatar Feb 16 '20 09:02 MikeRalphson

@MikeRalphson, you are correct, I used the wrong version in my initial description, however, the problem definitely exists in OAS3 (and is what I initially meant). Let me try and redeem myself:

Consider the following OAS3 configuration of a media-type versioned endpoint:

openapi: 3.0.1
info:
  title: Swagger Petstore
  description: '...'
  version: 1.0.0
tags:
- name: pets
  description: Everything about your Pets
paths:
  /pets/{id}:
    put:
      tags:
      - pets
      summary: Update an existing pet
      operationId: updatePet
      requestBody:
        description: Pet object that needs to be added to the store
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
          application/vnd.vendor.v1+json:
            schema:
              $ref: '#/components/schemas/Pet'
          application/vnd.vendor.v2+json:
            schema:
              $ref: '#/components/schemas/Pet_v2'
        required: true
      responses:
        400:
          description: Invalid ID supplied
          content: {}
        404:
          description: Pet not found
          content: {}
        405:
          description: Validation exception
          content: {}
      security:
      - petstore_auth:
        - write:pets

In the above yaml code block, the operation detailed shows three media-types listed. The first two are in support of version 1 of the endpoint, the last is for version 2 of the endpoint. The following issues become readily apparent:

  1. deprecation: How do we deprecate the v1 media-types? In the OAS3 specification, deprecation occurs at the operation level, not within a content block.
  2. documentation: How do we add documentation specific to version 2 of the endpoint? The OAS3 specification does not provide a method of augmenting/replacing the operation-level documentation.
  3. security: How do we specify different security requirements for different versions of an endpoint? Again, because OAS3 targets the operation as the unit of focus, there is no way to accomplish this.
  4. ... and on an on for each element of an operation.

Although initially it seems reasonable to consider a single operation the focus, when you apply the OAS3 specification to common web framework specifications, e.g. jax-rs, the problem becomes even more apparent. Consider this Jersey code:

@Path("pets")
public class PetsResource {

    @Path("{id}")
    @PUT
    @Consumes({"application/json", "application/vnd.vendor.v1+json"})
    @Operation(summary = "Updates an existing pet.",
            deprecated = true,
            responses = {
                    @ApiResponse(responseCode = "400", description = "Invalid ID supplied."),
                    @ApiResponse(responseCode = "404", description = "Pet not found.") },
            security = @SecurityRequirement(name="petstore_auth")
    )
    public void updatePet(@PathParam("id") int id, Pet pet) {
        // ...
    }

    @Path("{id}")
    @PUT
    @Consumes({"application/vnd.vendor.v2+json"})
    @Operation(summary = "Updates an existing pet with new pet extensions.",
            responses = {
                    @ApiResponse(responseCode = "400", description = "Invalid ID supplied."),
                    @ApiResponse(responseCode = "404", description = "Pet not found.") },
            security = @SecurityRequirement(name="petstore_auth")
    )
    public void updatePetV2(@PathParam("id") int id, PetV2 pet) {
        // ...
    }
}

In the above Jersey/JAX-RS code block, Jersey will route to the appropriate method, but Swagger-UI has the impossible task of determining which operation to extract into it's open-api.json file on build (currently, Swagger-UI only retains the last read path:method operation). Also, notice the discrepancies in the operation definition: summary and deprecated.

To address these issues, I propose:

  1. Making the API version in the root of the OpenAPI configuration the default version of each endpoint instead of the overall API version (overridden when the operation-details version is not specified - see next bullet item for details).
  2. Adding an array of operation-details to a path:operation. I'm defining an operation-detail object almost equivalent to the existing operation, with two exceptions: the tag element should remain on the parent object, and a version element should be added such that the version element is the unique identifier for the set of operation-details within a path:operation.
  3. Remove the current operation as the definition of a method in favor of a new object holding the tag element and versions element for the operation-details array.

With the above changes, the beginning yaml configuration would become:

openapi: 4.0.0 (I see no way for this not to be a breaking change)
info:
  title: Swagger Petstore
  description: '...'
tags:
- name: pets
  description: Everything about your Pets
paths:
  /pets/{id}:
    put:
      tags:
      - pets
      versions:
      - version: 1
        summary: Update an existing pet
        deprecated: true
        operationId: updatePet
        requestBody:
          description: Pet object that needs to be updated in the store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
            application/vnd.vendor.v1+json:
              schema:
                $ref: '#/components/schemas/Pet'
          required: true
        responses:
          400:
            description: Invalid ID supplied
            content: {}
          404:
            description: Pet not found
            content: {}
        security:
        - petstore_auth:
          - write:pets
      - version: 2
        summary: Update an existing pet with new extensions
        operationId: updatePet_v2
        requestBody:
          description: Pet object, with newly defined extensions, that needs to be updated in the store.
          content:
            application/vnd.vendor.v2+json:
              schema:
                $ref: '#/components/schemas/Pet_v2'
          required: true
        responses:
          404:
            description: Pet not found
            content: {}
        security:
        - petstore_auth:
          - write:pets

As the OAS3 specification currently stands, media-type versioning is implicitly discouraged in favor of path-versioning. I do not believe this was the intent, but it does have that affect.

Sorry for the initial confusion, I hope this clarifies things.

damonsutherland avatar Feb 16 '20 21:02 damonsutherland

Please don't forget to put the version of the request in a relation to the version of the response. A common case is that a service receiving a v1 request is only able to reply with a v1 response and v2 -> v2 (but not v1). Versions at path:method level would address this perfectly.

gadton avatar Feb 18 '20 10:02 gadton

@gadton Thanks for pointing out another deficiency in the current 3.x spec with regards to media type versioning. To ensure I completely understand, let me restate.

Consider media-type versioning with the current 3.x spec:

openapi: 3.0.1
...
paths:
  /pets:
    post:
      tags:
      - pets
      summary: Create a new pet
      operationId: createPet
      requestBody:
        description: Pet object that needs to be added to the store
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
          application/vnd.vendor.v1+json:
            schema:
              $ref: '#/components/schemas/Pet'
          application/vnd.vendor.v2+json:
            schema:
              $ref: '#/components/schemas/Pet_v2'
        required: true
      responses:
        200:
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
            application/vnd.vendor.v1+json:
              schema:
                $ref: '#/components/schemas/Pet'
            application/vnd.vendor.v2+json:
              schema:
                $ref: '#/components/schemas/Pet_v2'
      ...

In the above scenario, it is unclear what request content-type/response content-type combinations are legal. The best we could do is add a 406 response code with documentation indicating valid combinations. However, a move to versioning at the path:method level, we get this:

openapi: 4.0.0
...
paths:
  /pets:
    post:
      tags:
      - pets
      versions:
      - version: 1
        summary: Create a new pet
        operationId: createPet
        requestBody:
          description: Pet object that needs to be added to the store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
            application/vnd.vendor.v1+json:
              schema:
                $ref: '#/components/schemas/Pet'
          required: true
        responses:
          200:
            description: successful operation
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/Pet'
              application/vnd.vendor.v1+json:
                schema:
                  $ref: '#/components/schemas/Pet'
      - version: 2
        summary: Create a new version 2 pet
        operationId: createPet_v2
        requestBody:
          description: Pet object that needs to be added to the store
          content:
            application/vnd.vendor.v2+json:
              schema:
                $ref: '#/components/schemas/Pet_v2'
          required: true
        responses:
          200:
            description: successful operation
            content:
              application/vnd.vendor.v2+json:
                schema:
                  $ref: '#/components/schemas/Pet_v2'
      ...

The intent is clear.

damonsutherland avatar Feb 20 '20 15:02 damonsutherland

@MikeRalphson, when you have a moment, will you review my follow-up comments on this issue (#2142)? If I am still missing something, I am happy to close the issue. Thank you for your consideration.

damonsutherland avatar Feb 20 '20 15:02 damonsutherland

Versioning alone would not cut it.

Multiple mapping can be done on a single path, which are not simply different content types (e.g. application/json vs application/xml) nor different versions through content negotiation (e.g. application/vnd.something.v1+json vs application/vnd.something.v2+json).

A simple scenario where two (or more) functionalities reside on the same path and http method. Besides all other metadata (description, deprecated, etc), even items like tags should be moved into the subsections.

Below a quick example, where two GET operations delivery different data types and different data type versions on the same path and method:

  • User summary v1 (json and xml) (tagged as deprecated)
  • User summary v2 (json)
  • User detail v1 (json)
openapi: 4.0.0
...
paths:
  /users:
    get:
      - description: retrieves the user summaries (deprecated)
        tags:
          - user summary
        operationId: user summaries v1
        deprecated: true
        responses:
          200:
            content:
              application/vnd.vendor.user-summary.v1+json:
                schema:
                  $ref: '....'
              application/vnd.vendor.user-summary.v1+xml:
                schema:
                  $ref: '....'
      - description: retrieves the user summaries and adds this fancy new extra description information
        tags:
          - user summary
        operationId: user summaries v2
        responses:
          200:
            content:
              application/vnd.vendor.user-summary.v2+json:
                schema:
                  $ref: '....'
      - description: retrieves the user details of multiple users and this has a response with way more fields and also a different description and even a nice query param that the others don't have
        tags:
          - user detail
        operationId: user details v1
        parameters:
          - name: system
              in: query
              required: true
              schema:
                type: string
        responses:
          200:
            content:
              application/vnd.vendor.user-detail.v1+json:
                schema:
                  $ref: '....'

Hoagiex avatar Jul 22 '20 21:07 Hoagiex

deprecation: How do we deprecate the v1 media-types? In the OAS3 specification, deprecation occurs at the operation level, not within a content block.

I would:

  • deprecate the schema, not the operation. see http://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.3
  • return a content warning (eg. see https://tools.ietf.org/id/draft-cedik-http-warning-02.html it's not yet a standard)

documentation: How do we add documentation specific to version 2 of the endpoint There's no v2 endpoint imho, but a v2 media-type. The operation is the same.

it is unclear what request content-type/response content-type combinations are legal. The best we could do is add a 406 response code with documentation indicating valid combinations

This is the correct way to handle it, and the one which will better describe the endpoint.

How do we specify different security requirements for different versions of an endpoint

If you need different operations and security requirements, I'd use different paths or path-versioning then, because those are different operations.

My2¢, R

ioggstream avatar Oct 07 '20 15:10 ioggstream

I understand that this issue/feature request is still unresolved.

Still, I'm wondering: what is the current best practice? Path versioning should be quite common when your API starts to grow, so perhaps I'm misunderstanding some part of the specification, while the solution is obvious to others?

jacobwod avatar Mar 04 '21 06:03 jacobwod