На смену springfox пришел springdoc. Он приносит нам в проект Swagger и поддерживает спецификацию OpenApi 3. Но есть еще некоторые шерховатости, а именно правильное отображение параметров запроса для сортировки и постраничного вывода.

Давайте посмотрим, можно ли их исправить и как это сделать.

Проблема

После добавления в проект артефакта springdoc-openapi-ui становится доступна страница http://localhost:8080/swagger-ui.html
Все красиво и аккуратно кроме параметров типа Pageable и Sort.

    @GetMapping("/sort")
    public @ResponseBody void sort(Sort sort) {
        //
    }
    
    @GetMapping("/pageable")
    public @ResponseBody void pageable(Pageable pageable) {
        //
    }

Описание Pageable только выглядит странно, но пользоваться им можно.

Объект:

{
  "page": 0,
  "size": 1,
  "sort": [
    "string"
  ]
}

будет преобразован в набор параметров строки запроса:

curl -X 'GET' 
'http://localhost:8080/pageable?page=0&size=1&sort=string' 
-H 'accept: /'

А вот описание для Sort не соответствует действительности. Нам нужен параметр строки запроса sort, а не вот это:

К тому же в этих объектах параметрах нет описаний.

Устранение

Для Pageable все просто. Достаточно перед параметром метода аннотацию @ParameterObject

    @GetMapping("/pageable")
    public @ResponseBody void pageable(@ParameterObject Pageable pageable) {
        //
    }

Красивое...

Для Sort так сделать не получится. Но выход есть:

  1. Скрываем настоящий параметр с помощью аннотации @Parameter(hidden = true);

  2. Описываем правильный параметр самостоятельно.

Получается следующая конструкция:

    @Parameter(in = ParameterIn.QUERY, 
        description = "Sorting criteria in the format: property(,asc|desc). " +
                "Default sort order is ascending. " +
                "Multiple sort criteria are supported.",
        name = "sort",
        required = false,
        array = @ArraySchema(schema = @Schema(type = "string")))
    @GetMapping("/sort")
    public @ResponseBody void sort(@Parameter(hidden = true) Sort sort) {
        //
}

Теперь все выглядит так, как и задумывалось. Swagger можно использовать, а спецификацию OpenApi отдавать FrontEnd-разработчикам.

Надеюсь, что в скором времени для Sort тоже можно будет использовать аннотацию @ParameterObject.

Комментарии (5)


  1. Bromles
    01.04.2022 11:02
    +1

    При использовании этого решения натыкался на проблему: при наличии в контроллере более одного маппинга на один и тот же адрес, но с разными параметрами, в Сваггере в итоге все эти маппинги превращались в один эндпоинт, описание, схема и прочее которого случайно бралось от одного из маппингов, а в параметрах были абсолютно все параметры из всех маппингов по этому адресу, и с пометкой required (даже если в самих маппингах явно стоит required = false)

    Судя по всему, он просто мержит все эндпоинты на основании их адреса, а не методов с маппингами. В итоге вместо нескольких нормальных маппингов получается один, в который навалено все подряд в случайном порядке, да еще и вводящие в заблуждение параметры указаны

    Есть ли какой-то способ это починить?


    1. semo Автор
      01.04.2022 11:06

      Есть ссылка на гит* ? Без примеров кода сложно представить конструкцию.
      Не обещаю что починю, но посмотрю обязательно :)


      1. Bromles
        01.04.2022 11:11

        Я чуть ниже запостил код, если что-то еще понадобится - обращайтесь)


    1. Bromles
      01.04.2022 11:06
      +2

      То есть, код следующего вида:

      @ApiResponses(value = {
                  @ApiResponse(responseCode = "200", description = "Successful operation",
                          content = @Content(mediaType = "application/json",
                                  array = @ArraySchema(schema = @Schema(implementation = RegionDTO.class)))),
                  @ApiResponse(responseCode = "400", description = "Invalid name of region supplied", content = @Content),
                  @ApiResponse(responseCode = "404", description = "No regions found", content = @Content)
          })
          @GetMapping(params = { "name" })
          public ResponseEntity<Object> getByName(
                  @Parameter(description = "Name of required region")
                  @NotBlank(message = "Region name can't be blank")
                  @Size(max = 255, message = "Region name can't be longer than 255 characters")
                  @Pattern(regexp = "[а-яА-Я() -]+",
                          message = "Region name must contain only Cyrillic, spaces, dashes and brackets")
                  @RequestParam String name) throws RecordNotFoundException {
              List<RegionDTO> regionDTOs = regionDirectoryService.getByName(name);
              
              return ResponseEntity.ok(regionDTOs);
          }
      
      @ApiResponses(value = {
                  @ApiResponse(responseCode = "200", description = "Successful operation",
                          content = @Content(mediaType = "application/json",
                                  array = @ArraySchema(schema = @Schema(implementation = RegionDTO.class)))),
                  @ApiResponse(responseCode = "400", description = "Invalid beginning of region name supplied",
                          content = @Content),
                  @ApiResponse(responseCode = "404", description = "No regions found", content = @Content)
          })
          @GetMapping(params = { "name-beginning" })
          public ResponseEntity<Object> getByNameBeginning(
                  @Parameter(description = "Beginning of name of required region")
                  @Size(max = 255, message = "Beginning of region name can't be longer than 255 characters")
                  @Pattern(regexp = "[А-Я][а-я]*",
                          message = "Beginning of region name can't be blank, must contain only Cyrillic letters and " +
                                  "begins with Capital one")
                  @RequestParam("name-beginning") String nameBeginning) throws RecordNotFoundException {
              List<RegionDTO> regionDTOs = regionDirectoryService.getByNameBeginning(nameBeginning);
              
              return ResponseEntity.ok(regionDTOs);
          }
      
      @ApiResponses(value = {
                  @ApiResponse(responseCode = "200", description = "Successful operation",
                          content = @Content(mediaType = "application/json",
                                  array = @ArraySchema(schema = @Schema(implementation = RegionDTO.class)))),
                  @ApiResponse(responseCode = "400", description = "Invalid short name of region supplied", content =
                  @Content),
                  @ApiResponse(responseCode = "404", description = "No regions found", content = @Content)
          })
          @GetMapping(params = { "short-name" })
          public ResponseEntity<Object> getByShortName(
                  @Parameter(description = "Short name of required region")
                  @Pattern(regexp = "[А-Я]{3}",
                          message = "Region short name can't be blank and must be 3 Capital Cyrillic letters")
                  @RequestParam("short-name") String shortName) throws RecordNotFoundException {
              List<RegionDTO> regionDTOs = regionDirectoryService.getByShortName(shortName);
              
              return ResponseEntity.ok(regionDTOs);
          }

      Превращается в такой вот эндпоинт:


      1. semo Автор
        01.04.2022 11:26
        +1

        Все верно. С точки зрения REST и OpenApi - это и есть один эндпоинт, так как имеет общий uri.

        Посмотрите спецификацию. Все начинается с path. Попробуйте сформулировать свое желание сначала в виде спецификации https://editor.swagger.io/ а потом сгенерировать код.

        Я вижу интересный вариант по разнесению одного эндпоинта за счет применения params, но тогда для того чтобы это работало нужно:
        1. Указать параметры для спецификации как 'required = false'
        2. Предусмотреть вариант когда пользователь не ввел никаких параметров "поиска". Наверняка в вашем контроллере есть что-то типа getAll() без параметров и он тоже попал в этот эндпоинт.

        Я бы предложил пересмотреть подход и вместо вызова отдельных методов репозитория использовать спецификации и один энпоинт `/search`