swagger-parser icon indicating copy to clipboard operation
swagger-parser copied to clipboard

[Bug]: Swagger Parser generates duplicate components with sequential suffixes when using mixed reference patterns

Open dbr-cbk opened this issue 5 months ago • 3 comments

Description

When using swagger-parser to resolve and bundle an OpenAPI specification that references external fragments, the library generates duplicate components with sequential suffixes (e.g., EmployeeInfo_1) even when there are no actual naming conflicts in the source files.

Affected Version

2.1.27

Steps to Reproduce

  1. Create a main OpenAPI specification file (main.yaml) that references schemas from an external fragment
  2. Create a fragment file (fragments.yaml) with a component used in multiple contexts: 2.1. As intermediate references in requests (PostEmployeeRequest → EmployeeInfo) 2.2. As direct references in responses (EmployeeInfo200 → EmployeeInfo) 2.3. As array element references (EmployeesInfoPage.content.items → EmployeeInfo)
  3. Use swagger-parser to resolve and bundle the specification
  4. Observe that duplicate components with suffixes are generated

main.yaml

openapi: "3.0.0"
info:
  title: Test API
  version: 1.0.0
paths:
  /employees:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: 'fragments.yaml#/components/schemas/PostEmployeeRequest'
      responses:
        '201':
          $ref: 'fragments.yaml#/components/responses/EmployeeInfo201'
  /employees/id:
    put:
      requestBody:
        content:
          application/json:
            schema:
              $ref: 'fragments.yaml#/components/schemas/PutEmployeeRequest'
      responses:
        '200':
          $ref: 'fragments.yaml#/components/responses/EmployeeInfo200'
  /employees/extended:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: 'fragments.yaml#/components/schemas/EmployeesInfoPage'

fragments.yaml

openapi: "3.0.0"
info:
  title: Fragments
  version: 1.0.0
components:
  schemas:
    EmployeeInfo:
      required:
        - email
        - employeeId
      type: object
      properties:
        employeeId:
          type: string
          description: Identifier of the employee.
          example: 123e4567-e89b-12d3-a456-426614174000_1729785600000
        email:
          type: string
          format: email
          example: [email protected]
        role:
          type: string
          enum: ["REQUESTER", "ADMIN", "DEVELOPER"]
          example: ADMIN
      description: Schema used for defining the principal information of an employee.
    
    PostEmployeeRequest:
      $ref: '#/components/schemas/EmployeeInfo'
    
    PutEmployeeRequest:
      $ref: '#/components/schemas/EmployeeInfo'
    
    EmployeesInfoPage:
      type: object
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/EmployeeInfo'
        totalElements:
          type: integer
          example: 100
  
  responses:
    EmployeeInfo200:
      description: OK
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EmployeeInfo'
    
    EmployeeInfo201:
      description: Created
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EmployeeInfo'

Result (note "EmployeeInfo_1):

openapi: 3.0.0
info:
  title: Test API
  version: 1.0.0
servers:
- url: /
paths:
  /employees:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostEmployeeRequest'
      responses:
        "201":
          $ref: '#/components/responses/EmployeeInfo201'
  /employees/id:
    put:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PutEmployeeRequest'
      responses:
        "200":
          $ref: '#/components/responses/EmployeeInfo200'
        "201":
          $ref: '#/components/responses/EmployeeInfo201'
  /employees/extended:
    get:
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmployeesInfoPage'
components:
  schemas:
    PostEmployeeRequest:
      $ref: '#/components/schemas/EmployeeInfo'
    EmployeeInfo:
      required:
      - email
      - employeeId
      type: object
      properties:
        employeeId:
          type: string
          description: Identifier of the employee.
          example: 123e4567-e89b-12d3-a456-426614174000_1729785600000
        email:
          type: string
          description: Email of the employee.
          format: email
          example: [email protected]
        groupTags:
          $ref: '#/components/schemas/ArrayOfGroups'
        role:
          type: string
          description: "Role of the employee inside of PDH. Three possible values:\
            \ REQUESTER, ADMIN and DEVELOPER"
          example: ADMIN
          enum:
          - REQUESTER
          - ADMIN
          - DEVELOPER
      description: Schema used for defining the principal information of an employee.
    ArrayOfGroups:
      type: array
      description: Schema used for defining an array of groups
      items:
        $ref: '#/components/schemas/GroupTags'
    GroupTags:
      type: object
      properties:
        groupTag:
          type: string
          example: group-1
    EmployeeInfo_1:
      required:
      - email
      - employeeId
      type: object
      properties:
        employeeId:
          type: string
          description: Identifier of the employee.
          example: 123e4567-e89b-12d3-a456-426614174000_1729785600000
        email:
          type: string
          description: Email of the employee.
          format: email
          example: [email protected]
        groupTags:
          $ref: '#/components/schemas/ArrayOfGroups'
        role:
          type: string
          description: "Role of the employee inside of PDH. Three possible values:\
            \ REQUESTER, ADMIN and DEVELOPER"
          example: ADMIN
          enum:
          - REQUESTER
          - ADMIN
          - DEVELOPER
      description: Schema used for defining the principal information of an employee.
    PutEmployeeRequest:
      $ref: '#/components/schemas/EmployeeInfo'
    EmployeesInfoPage:
      type: object
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/EmployeeInfo_1'
        pageResponse:
          $ref: '#/components/schemas/PageResponse'
      description: Schema used for defining a page of employees containing also pagination
        metadata.
    PageResponse:
      type: object
      properties:
        totalElements:
          type: integer
          example: 100
        size:
          type: integer
          example: 20
  responses:
    EmployeeInfo201:
      description: Created
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EmployeeInfo'
    EmployeeInfo200:
      description: OK
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EmployeeInfo'

Expected Behavior

The bundled specification should contain only one EmployeeInfo component, since:

  • All references point to the same component definition
  • No naming conflicts exist in any source file
  • The component is defined only once

Workaround Applied

To prevent automatic suffix generation and gain control over component naming: 1- Create context-specific components:

Use EmployeeInfoRequest for all request schemas
Use EmployeeInfo for all response schemas
  1. Update references by context:

Result: This prevents suffix generation but requires maintaining duplicate component definitions, significantly reducing maintainability.

Impact This behavior forces developers to choose between:

  • Accepting unpredictable component names with auto-generated suffixes
  • Duplicating component definitions to control naming (reducing maintainability)

Actual Behavior

Swagger-parser generates a bundled specification with:

  • EmployeeInfo (original component)
  • EmployeeInfo_1 (duplicate with suffix)
  • Request components (PostEmployeeRequest, PutEmployeeRequest) reference EmployeeInfo_1
  • Response components reference the original EmployeeInfo

Checklist

  • [x] I have searched the existing issues and this is not a duplicate (although similar issues were reported -e.g. #1081).
  • [x] I have provided sufficient information for maintainers to reproduce the issue.

dbr-cbk avatar Aug 06 '25 14:08 dbr-cbk

Hi @dbr-cbk I have tried to reproduce the behavior you had reported but failed. Here's the test I have written (main.yaml and fragments.yaml are copied from this issue). Can you please verify if this is the same case as yours?

@Test
    public void testIssue2217ExternalReferenceResolution() {
        OpenAPIV3Parser openApiParser = new OpenAPIV3Parser();
        ParseOptions options = new ParseOptions();
        options.setResolve(true);
        options.setFlatten(true);

        SwaggerParseResult parseResult = openApiParser.readLocation("issue2218/main.yaml", null, options);
        OpenAPI openAPI = parseResult.getOpenAPI();

        Assert.assertNotNull(openAPI, "OpenAPI should be parsed successfully");

        // Assert that EmployeeInfo_1 object does not appear in the parsed spec
        Assert.assertNotNull(openAPI.getComponents(), "Components should exist");
        if (openAPI.getComponents().getSchemas() != null) {
            Assert.assertFalse(openAPI.getComponents().getSchemas().containsKey("EmployeeInfo_1"),
                "EmployeeInfo_1 should not be created during external reference resolution");
        }
    }`

ewaostrowska avatar Aug 12 '25 13:08 ewaostrowska

Hi @ewaostrowska, You are right, the sample YAML files I previously provided do not generate the element. Please find attached updated versions of the files which do produce the EmployeeInfo_1 node in the resulting contract.

main.yaml

openapi: "3.0.0"
info:
  title: Main test
  version: 1.1.0
paths:
  /int/employees/extended:
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: 'fragments.yaml#/components/schemas/EmployeesInfoPage'
  /int/employees:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: 'fragments.yaml#/components/schemas/PostEmployeeRequest'
        required: true
      responses:
        "201":
          $ref: 'fragments.yaml#/components/responses/EmployeeInfo201'
  /int/employees/id:
    put:
      requestBody:
        content:
          application/json:
            schema:
              $ref: 'fragments.yaml#/components/schemas/PutEmployeeRequest'
        required: true
      responses:
        "200":
          $ref: 'fragments.yaml#/components/responses/EmployeeInfo200'
        "201":
          $ref: 'fragments.yaml#/components/responses/EmployeeInfo201'
  /int/employees/id/request:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: 'fragments.yaml#/components/schemas/EmployeeIdRequest'
        required: true
      responses:
        "200":
          $ref: 'fragments.yaml#/components/responses/EmployeeInfo200'
  /int/employees/id/remove:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: 'fragments.yaml#/components/schemas/EmployeeIdRequest'
        required: true
      responses:
        "204":
          description: No Content

fragments.yaml

openapi: "3.0.0"
info:
  version: 1.1.0
  title: fr_fragments
paths: {}
components:
  schemas:
    CreatedByEmployeeId:
      type: string
    EmployeesInfoPage:
      type: object
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/EmployeeInfo'
        pageResponse:
          $ref: pagination.yaml#/components/schemas/PageResponse
    ArrayOfGroupsInfo:
      type: array
      items:
        $ref: '#/components/schemas/GroupInfo'
    EmployeeInfo:
      type: object
      properties:
        employeeId:
          type: string
        groupTags:
          $ref: '#/components/schemas/ArrayOfGroups'
    ArrayOfGroups:
      type: array
      items:
        $ref: '#/components/schemas/GroupTags'
    GroupTags:
      type: object
      properties:
        groupTag:
          $ref: '#/components/schemas/GroupId'
    GroupId:
      type: string
    PostEmployeeRequest:
      $ref: '#/components/schemas/EmployeeInfo'
    PutEmployeeRequest:
      $ref: '#/components/schemas/EmployeeInfo'
    EmployeeIdRequest:
      type: object
      properties:
        employeeId:
          type: string
  responses:
    EmployeeInfo200:
      description: OK
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EmployeeInfo'
    EmployeeInfo201:
      description: Created
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EmployeeInfo'

Result (note "EmployeeInfo_1):

info:
  title: Main test
  version: 1.1.0
servers:
- url: /
paths:
  /int/employees/extended: {}
  /int/employees:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostEmployeeRequest'
        required: true
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmployeeInfo'
  /int/employees/id:
    put:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PutEmployeeRequest'
        required: true
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmployeeInfo'
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmployeeInfo'
  /int/employees/id/request:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmployeeIdRequest'
        required: true
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmployeeInfo'
  /int/employees/id/remove:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmployeeIdRequest'
        required: true
      responses:
        "204":
          description: No Content
components:
  schemas:
    PostEmployeeRequest:
      $ref: '#/components/schemas/EmployeeInfo'
    EmployeeInfo:
      type: object
      properties:
        employeeId:
          type: string
        groupTags:
          $ref: '#/components/schemas/ArrayOfGroups'
    ArrayOfGroups:
      type: array
      items:
        $ref: '#/components/schemas/GroupTags'
    GroupTags:
      type: object
      properties:
        groupTag:
          $ref: '#/components/schemas/GroupId'
    GroupId:
      type: string
    EmployeeInfo_1:
      type: object
      properties:
        employeeId:
          type: string
        groupTags:
          $ref: '#/components/schemas/ArrayOfGroups'
    PutEmployeeRequest:
      $ref: '#/components/schemas/EmployeeInfo'
    EmployeeIdRequest:
      type: object
      properties:
        employeeId:
          type: string
  responses:
    EmployeeInfo201:
      description: Created
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EmployeeInfo'
    EmployeeInfo200:
      description: OK
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EmployeeInfo'

Best regards.

dbr-cbk avatar Aug 26 '25 08:08 dbr-cbk

Should be fixed with #2105

jhemelhof avatar Nov 08 '25 14:11 jhemelhof