Skip to content

[BUG][openapi-yaml] Merged spec reorders HTTP operations under the same path, breaking intended method ordering #23009

@simonenkoi

Description

@simonenkoi

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

When using the openapi-yaml generator to produce a merged (flattened) OpenAPI specification with all $ref references resolved, the HTTP operations (methods) under each path are reordered alphabetically instead of preserving the original declaration order from the source YAML file.

For example, if a path defines operations in the order GET, POST, PUT, PATCH, DELETE, the merged output reorders them to DELETE, GET, PATCH, POST, PUT. This silently breaks the intended ordering despite the source OpenAPI files being defined correctly.

We maintain internal API documentation conventions that prescribe a specific HTTP method order (e.g., CRUD: GET -> POST -> PUT -> PATCH -> DELETE). The merged spec is used as the source of truth for downstream API consumers and documentation tools (e.g., Swagger). Because the generator reorders operations, consumers see them in an unexpected sequence, leading to confusion and inconsistency between the authored spec and the rendered documentation.

openapi-generator version

Reproduced with v7.14.0. The issue likely affects all versions of the openapi-yaml generator that do not set skipSortingOperations.

OpenAPI declaration file content or url

Input spec (sample-api-v1.yaml):

openapi: 3.0.3
info:
  title: Sample API
  version: 1.0.0
tags:
  - name: orders
    description: Order management operations
paths:
  /orders:
    get:
      tags:
        - orders
      summary: List all orders
      operationId: listOrders
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Order'
    post:
      tags:
        - orders
      summary: Create a new order
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Order'
      responses:
        '201':
          description: order created
  /orders/{orderId}:
    get:
      tags:
        - orders
      summary: Get order by ID
      operationId: getOrderById
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
    put:
      tags:
        - orders
      summary: Update an order
      operationId: updateOrder
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Order'
      responses:
        '200':
          description: order updated
    patch:
      tags:
        - orders
      summary: Partially update an order
      operationId: patchOrder
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Order'
      responses:
        '200':
          description: order patched
    delete:
      tags:
        - orders
      summary: Delete an order
      operationId: deleteOrder
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: order deleted
components:
  schemas:
    Order:
      type: object
      required:
        - id
      properties:
        id:
          type: string
        description:
          type: string
        status:
          type: string
          enum:
            - pending
            - completed
            - cancelled
Generation Details
java -jar openapi-generator-cli.jar generate \
  -i sample-api-v1.yaml \
  -g openapi-yaml \
  -o ./output

Or via Gradle plugin:

tasks.register<GenerateTask>("generateMergedSpec") {
    generatorName.set("openapi-yaml")
    inputSpec.set("$projectDir/api/sample-api-v1.yaml")
    outputDir.set("${layout.buildDirectory.get().asFile}/merged-spec")
    configOptions.put("outputFile", "sample-api-v1.yaml")
}
Steps to reproduce
  1. Save the YAML spec above as sample-api-v1.yaml.
  2. Generate the merged spec using the openapi-yaml generator (via CLI or Gradle plugin).
  3. Open the generated output file (e.g. build/merged-spec/sample-api-v1.yaml for the Gradle example, or output/openapi/openapi.yaml for the CLI).
  4. Inspect the HTTP methods under /orders/{orderId}.

Expected: Operations preserve the source file order:

  /orders/{orderId}:
    get:
      ...
    put:
      ...
    patch:
      ...
    delete:
      ...

Actual: Operations are reordered alphabetically by HTTP method:

  /orders/{orderId}:
    delete:
      ...
    get:
      ...
    patch:
      ...
    put:
      ...

This causes documentation tools to display operations in an unintuitive order (DELETE before GET), creating confusion for API consumers who see a different ordering than what was intentionally defined in the source spec.

Related issues/PRs
Suggest a fix

The openapi-yaml generator (OpenAPIYamlGenerator) should override getSkipSortingOperations() to return true by default, similar to the fix applied in #22163 for python-fastapi.

The openapi-yaml generator is primarily used to resolve $ref references and produce a flat, self-contained YAML file. Sorting operations is counterproductive in this use case, as the purpose is to faithfully represent the original spec with resolved references, not to rearrange it.

Alternatively, a global skipSortingOperations config option could be exposed so users of any generator can opt out of operation sorting.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions