Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The operationId is unnecessarily deduplicated for a requestBody with multiple content types #2646

Closed
scrhartley opened this issue Jul 14, 2024 · 4 comments
Labels
enhancement New feature or request

Comments

@scrhartley
Copy link

Describe the bug

In a Spring Boot RestController that wants to support POST request bodies of both form data or JSON,
to use the built-in functionality, multiple endpoint methods must be used to support both content types.
The JSON accepting method expects the Spring MVC @RequestBody annotation to be used,
while the form data version expects Spring MVC @RequestBody to not be used, so the Swagger annotation must be used instead.

Although the output OpenAPI JSON correctly puts the two as the same request body with different supported content, the operationId is incorrectly deduplicated, changing its value.
n.b. This does require the operationId to be set explicitly on both methods, because otherwise the last automatically generated operationId will be picked to represent both methods.

To Reproduce

  • What version of spring-boot you are using?
    3.3.1
  • What modules and versions of springdoc-openapi are you using?
    springdoc-openapi-starter-webmvc-api version 2.6.0,
    creating OpenAPI version 3.0.1 JSON.
  • What is the actual and the expected result using OpenAPI Description (yml or json)?
    Actual (snippet):
"post": {
    "tags": [
        "books"
    ],
    "operationId": "create_1_1",
    "requestBody": {
        "content": {
            "application/x-www-form-urlencoded": {
                "schema": {
                    "$ref": "#/components/schemas/Book"
                }
            },
            "application/json": {
                "schema": {
                    "$ref": "#/components/schemas/Book"
                }
            }
        },
        "required": true
    },
    "responses": {
        "200": {
            "description": "OK"
        }
    }
}

Expected the same except:

    "operationId": "create",
  • Provide with a sample code (HelloController) or Test that reproduces the problem
@RestController
@RequestMapping("books")
public class DemoController {

    @PostMapping(value = "/book", consumes = APPLICATION_JSON_VALUE)
    @Operation(operationId = "create", tags = "books")
    public void createBookFromJson(
            @org.springframework.web.bind.annotation.RequestBody Book book) {
        System.out.println("creating book: " + book);
    }

    @PostMapping(value = "/book", consumes = APPLICATION_FORM_URLENCODED_VALUE)
    @Operation(operationId = "create", tags = "books")
    public void createBookFromFormData(
            @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true) Book book) {
        createBookFromJson(book);
    }

    record Book(@NotNull String title, @NotNull String author) {}

}

Expected behavior

  • When the different content types are merged for a single requestBody, this should not trigger deduplication and the originally specified operationId should be used.
@grimly
Copy link

grimly commented Jul 17, 2024

Hello,
The same apply for multiple response content type.

@grimly
Copy link

grimly commented Jul 17, 2024

Here is a customizer that can circumvent the issue until resolved but then, be careful, it removes any deduplication capabilities:

@Component
public class PreserveOperationIdOpenApiCustomizer implements GlobalOperationCustomizer {
  @Override
  public Operation customize(Operation operation, HandlerMethod handlerMethod) {
    operation.setOperationId(
      Optional
        .ofNullable(handlerMethod.getMethodAnnotation(io.swagger.v3.oas.annotations.Operation.class))
        .map(io.swagger.v3.oas.annotations.Operation::operationId)
        .orElseGet(handlerMethod.getMethod()::getName)
    );
    return operation;
  }
}

@scrhartley
Copy link
Author

I ended up with a less aggressive customizer:

@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
    Optional
        .ofNullable(handlerMethod.getMethodAnnotation(io.swagger.v3.oas.annotations.Operation.class))
        .map(io.swagger.v3.oas.annotations.Operation::operationId)
        .ifPresent(operation::setOperationId);
    return operation;
}

@bnasslahsen bnasslahsen added the enhancement New feature or request label Sep 22, 2024
@scrhartley
Copy link
Author

Thank you, I'll look forward to the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants