Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Data Rest: Change Operation ID in OpenAPI Specification

I am trying to generate an openapi.yaml of my Spring Data Rest service so we can easily generate client code with the typescript-angular generator. Unfortunately, the names of generated services and methods are ... less than ideal. We get different controllers for the entity, for "search" and another for relation. Also the function names in the generated services are extremely long without adding much information / benefit. Here's an example:

paths:
  /pricingPlans:
    get:
      tags:
      - pricing-plan-entity-controller
      description: get-pricingplan
      operationId: getCollectionResource-pricingplan-get_1

With this openapi.yaml, we get a PricingPlanEntityControllerService with a function getCollectionResource-pricingplan-get_1 which is just ridiculous. We would like to change that to PricingPlanService and getAll.

@Tag(name = "pricing-plan")
@CrossOrigin
public interface PricingPlanRepo extends CrudRepository<PricingPlan, UUID> {

    @Override
    Iterable<PricingPlan> findAll();

By adding @Tag(name = "pricing-plan") at the class level, we were able to change the name of the generated service to PricingPlanService but no matter what we tried, the operationId always remains as before.

I would expect @Operation(operationId = "getAll") to do what we want, but as I said: ignored. What's the proper way to apply all these annotations to with with Spring Data Rest?

like image 822
user3235738 Avatar asked May 08 '26 10:05

user3235738


2 Answers

Note that your approach of using the @Operation annotation to customize the operation id, doesn't work for the majority of spring-data-rest repositories: The reason, is that the operations are generated internally by the framework and you don't have any way to add the annotations.

The simple way, that works in all the cases is using the OpenApiCustomiser, to change the value of any part of the generated OpenAPI spec as described in the documentation.

    @Bean
    OpenApiCustomiser operationIdCustomiser() {
        return openApi -> openApi.getPaths().values().stream().flatMap(pathItem -> pathItem.readOperations().stream())
                .forEach(operation -> {
                    if ("id-to-change".equals(operation.getOperationId()))
                        operation.setOperationId("any id you want ...");
                });
    }

@mimi78 pointed me to how to customize the generated OpenAPI specification. Thanks for that! I was concerned with the method of simply adding 1:1 translations of operation IDs as the internal/original name might change as endpoints get added or removed. I came up with a solution that generates the operation ID from the path pattern (e.g. /products/{id}/vendor) and the HTTP method. I think this should provide stable naming that is human readable and better suited for client generators that base their code on the operation ID.

I wanted to share that solution in case someone else needs it one day:

@Configuration
public class OperationIdCustomizer {

    @Bean
    public OpenApiCustomiser operationIdCustomiser() {
        // @formatter:off
        return openApi -> openApi.getPaths().entrySet().stream()
            .forEach(entry -> {
                String path = entry.getKey();
                PathItem pathItem = entry.getValue();
                if (pathItem.getGet() != null)
                    pathItem.getGet().setOperationId(OperationIdGenerator.convert("get", path));
                if (pathItem.getPost() != null)
                    pathItem.getPost().setOperationId(OperationIdGenerator.convert("post", path));
                if (pathItem.getPut() != null)
                    pathItem.getPut().setOperationId(OperationIdGenerator.convert("put", path));
                if (pathItem.getPatch() != null)
                    pathItem.getPatch().setOperationId(OperationIdGenerator.convert("patch", path));
                if (pathItem.getDelete() != null)
                    pathItem.getDelete().setOperationId(OperationIdGenerator.convert("delete", path));
            });
        // @formatter:on
    }

}
public class OperationIdGenerator {

    private static String pattern1 = "^/([a-zA-Z]+)$"; // /products
    private static String pattern2 = "^/([a-zA-Z]+)/(\\{[a-zA-Z]+\\})$"; // /products/{id}
    private static String pattern3 = "^/([a-zA-Z]+)/(\\{[a-zA-Z]+\\})/([a-zA-Z]+)$"; // /products/{id}/vendor
    private static String pattern4 = "^/([a-zA-Z]+)/(\\{[a-zA-Z]+\\})/([a-zA-Z]+)/(\\{[a-zA-Z]+\\})$"; // /products/{id}/vendor/{propertyId}
    private static String pattern5 = "^/([a-zA-Z]+)/search/([a-zA-Z]+)$"; // /products/search/findByVendor

    // @formatter:off
    private static Map<String, String> httpMethodVerb = Map.of(
        "get",      "get",
        "post",     "create",
        "put",      "replace",
        "patch",    "update",
        "delete",   "delete");
    // @formatter:on

    private static String handlePattern1(String op, String path) {
        Pattern r = Pattern.compile(pattern1);
        Matcher m = r.matcher(path);
        boolean found = m.find();
        if (!found)
            return null;
        String noun = toCamelCase(m.group(1));
        String verb = getVerb(op);
        if (verb.equals("create"))
            noun = singularize(noun);
        return verb + noun;
    }

    private static String handlePattern2(String op, String path) {
        Pattern r = Pattern.compile(pattern2);
        Matcher m = r.matcher(path);
        boolean found = m.find();
        if (!found)
            return null;
        String noun = toCamelCase(singularize(m.group(1)));
        return getVerb(op) + noun;
    }

    private static String handlePattern3(String op, String path) {
        Pattern r = Pattern.compile(pattern3);
        Matcher m = r.matcher(path);
        boolean found = m.find();
        if (!found)
            return null;
        String noun = toCamelCase(singularize(m.group(1)));
        String relation = toCamelCase(m.group(3));
        return op + noun + relation;
    }

    private static String handlePattern4(String op, String path) {
        Pattern r = Pattern.compile(pattern4);
        Matcher m = r.matcher(path);
        boolean found = m.find();
        if (!found)
            return null;
        
        String entity = toCamelCase(singularize(m.group(1)));
        String relation = m.group(3);

        return getVerb(op) + entity + toCamelCase(singularize(relation)) + "ById";
    }

    private static String handlePattern5(String op, String path) {
        Pattern r = Pattern.compile(pattern5);
        Matcher m = r.matcher(path);
        boolean found = m.find();
        if (!found)
            return null;
        String entity = toCamelCase(m.group(1));
        String searchMethod = m.group(2);
        r = Pattern.compile("findBy([a-zA-Z0-9]+)");
        m = r.matcher(searchMethod);
        if (m.find())
            return "search" + entity + "By" + toCamelCase(m.group(1));
        return "search" + entity + "By" + toCamelCase(searchMethod);
    }

    public static String singularize(String word) {
        Inflector i = new Inflector();
        return i.singularize(word);
    }

    public static boolean isSingular(String word) {
        Inflector i = new Inflector();
        return i.singularize(word).equals(word);
    }

    public static String getVerb(String op) {
        return httpMethodVerb.get(op);
    }

    public static String convert(String op, String path) {
        String result = handlePattern1(op, path);
        if (result == null) {
            result = handlePattern2(op, path);
            if (result == null) {
                result = handlePattern3(op, path);
                if (result == null) {
                    result = handlePattern4(op, path);
                    if (result == null) {
                        result = handlePattern5(op, path);
                    }
                }
            }
        }
        return result;
    }

    private static String toCamelCase(String phrase) {
        List<String> words = new ArrayList<>();
        for (String word : phrase.split("_"))
            words.add(word);
        for (int i = 0; i < words.size(); i++) {
            String word = words.get(i);
            String firstLetter = word.substring(0, 1).toUpperCase();
            word = firstLetter + word.substring(1);
            words.set(i, word);
        }
        return String.join("", words.toArray(new String[words.size()]));
    }

}
like image 40
user3235738 Avatar answered May 12 '26 04:05

user3235738



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!