Некоторое время назад, решили мы поменять тошнотворный Swagger на что-то современное. Остановились на Scalar-е. Современный, компактный и возможностей побольше. Здесь постараюсь рассказать с чем столкнулись при этом.
Действующие лица
- Текущий проект. На котором API документация показывается в Swagger-е
- OpenAPI. Спецификация по которой генеруется структура и описание REST API. Собственно то во что развился Swagger.
- Swagger. В нашем случае UI, который отображает данные сгенерированные по стандарту OpenAPI.
- Scalar. Просто UI, который отображает данные сгенерированные по стандарту OpenAPI и на который мы хотим смигрировать Swagger.
- Springdoc. Прослойка между SpringBoot и нашей документацией
Инициируем типа наш текущий проект со Swagger-ом
- Идем на сайт Spring, забираем SpringBoot3 приложение и добавляем в IDE
- Исправляем зависимость:
1implementation 'org.springframework.boot:spring-boot-starter'
на
1implementation 'org.springframework.boot:spring-boot-starter-web' - Создаём котроллер, который что-то возвращает:
123456789@RestControllerpublic class DemoController {@GetMapping("/demo")public String getData() {return "$$$ Some response " + new Date();}} - Запускаем приложение и идем по адресу localhost:8080/demo. Видим результат:
123456789101112131415161718192021curl http://localhost:8080/demoStatusCode : 200StatusDescription :Content : $$$ Some response Sun Jan 25 14:41:32 EET 2026RawContent : HTTP/1.1 200Keep-Alive: timeout=60Connection: keep-aliveContent-Length: 46Content-Type: text/plain;charset=UTF-8Date: Sun, 25 Jan 2026 12:41:32 GMT$$$ Some response Sun Jan 25 14:41:32 ...Forms : {}Headers : {[Keep-Alive, timeout=60], [Connection, keep-alive], [Content-Length, 46], [Content-Type, text/plain;charset=UTF-8]...}Images : {}InputFields : {}Links : {}ParsedHtml : System.__ComObjectRawContentLength : 4
У нас есть приложение.
Получаем OpenAPI спецификацию
Здесь как раз нам помогает Springdoc:
- Добавляем зависимость:
1implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.13' - Добавляем в application.yaml
1234springdoc:api-url: http://localhost:8080api-docs:path: /v3/api-docs - Идем по адресу localhost:8080/demo. Видим результат:
1234567891011121314151617181920curl http://localhost:8080/v3/api-docsStatusCode : 200StatusDescription :Content : {"openapi":"3.1.0","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"paths":{"/demo":{"get":{"tags":["demo-controll...RawContent : HTTP/1.1 200Content-Length: 336Content-Type: application/jsonDate: Sun, 25 Jan 2026 13:09:31 GMT{"openapi":"3.1.0","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"ht...Forms : {}Headers : {[Content-Length, 336], [Content-Type, application/json], [Date, Sun, 25 Jan 2026 13:09:31 GMT]}Images : {}InputFields : {}Links : {}ParsedHtml : System.__ComObjectRawContentLength : 336
Как можно заметить, мы получили в ответ JSON файл, который собственно и содержит всю документацию о моем эндпоинте. Теперь его можно скармливать в любой UI или клиент. Они сами будут его парсить и отображать.
Swagger
- Добавляем зависимость:
1implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13' - Идем по адресу localhost:8080/swagger-ui. Видим результат:
Ностальгируем по 2015 году.
Вот в таком состоянии была наша документация перед миграцией на Scalar.
Первый подход. Дёшево, надежно, практично
Переход на Scalar у нас осуществлялся одновременно с разделением наших эндпоинтов на две разные группы, которые изначально предполагалось прятать за ролевой моделью. Соответственно нужно было иметь две разные непересекающиеся OpenAPI спецификации. Помогает в этом опять же Springdoc:
- Добавляем конфигурацию:
1234567891011121314151617181920@Configurationpublic class OpenApiConfiguration {@Beanpublic GroupedOpenApi adminApi() {return GroupedOpenApi.builder().group("admin-group").packagesToScan("com.delfin.demo.scalar.controller.admin").build();}@Beanpublic GroupedOpenApi demoApi() {return GroupedOpenApi.builder().group("demo-group").packagesToScan("com.delfin.demo.scalar.controller.demo").build();}}
Как видно эндпоинты мы группируем по пакетам - Идем поадресу localhost:8080/swagger-ui. Получаем:

Как можно заметить, получился комбобоксик в правом верхнем углу, где можно переключаться между разными группами эндпоинтов. По началу это меня не устраивало, поскольку невозможно было разделить по ролям для отображения. Для этого нужно было иметь два разных эндпоинта, которые можно было бы подключать к соответствующим OpenAPI ссылкам.
Решение с мигрцией на Scalar, которое нашлось в сети, получилось весьма простым, топорным и очень гибким. Нужно просто на своей стороне генерировать HTML код:
- Убираем зависимость:
1implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13' - Создаём свой собственный Scalar контроллер:
12345678910111213141516171819202122232425262728293031323334@RestController@RequestMapping(value = "/scalar-ui")public class ScalarController {@GetMapping("/admin")@ResponseBodypublic String getAdminUi() {return makeHtml("Admin API", "admin-group");}@GetMapping("/demo")@ResponseBodypublic String getDemoUi() {return makeHtml("Demo API", "demo-group");}private static String makeHtml(String title, String group) {return """<!doctype html><html><head><title>%s</title><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /></head><body><script id="api-reference" data-url="/v3/api-docs/%s"></script><script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script></body></html>""".formatted(title, group);}}
Как это работает. Браузер получает HTML, загружает JavaScrip в строке 28, который в свою очередь получает конфигруцию из строки 27 и отрисовывает UI. А в строке 27 хранится пока путь к соответствующей OpenAPI документации.Также есть возможность модифицировать HTML. Например, задать нужное название вкладки.
- Идём по соотвествующим ссылкам. Видим необходимые документации:

Вуаля! Всё работает.
Но. В процессе код-ревью выясняется, что мы не хотим генерировать HTML и не хотим прятать документацию по ролям (т.е. все должны видеть всё). Начитается поиск иного пути.
Современный программист больше конфигуратор
Правильно. Меньше кода – больше конфигурации. Приходится открывать документацию по Scalar-у и читать ((
Итак, делаем как написано в доке:
- Добавляем зависимость:
1implementation 'com.scalar.maven:scalar:0.1.0' - Добавляем свойства:
1234scalar:url: /v3/api-docspath: /docsenabled: true - Идём по адресу localhost:8080/docs
И пара-пара-пам! Приплыли! Сливаем воду! Иду на https://mvnrepository.com/artifact/com.scalar.maven/scalar. Вижу последнюю версию 0.4.4 (версии 0.1.0 нет вообще!). Пробую 0.4.4 – без изменений. Оригинальная документация бесполезна!
Конфигуратор не сдаётся никогда!
Ещё варианты? Кто-то где-то слышал, что Springdoc поддерживает Scalar. Удивительно но факт. Начинаю подключать Scalar через Springdoc:
- Удаляем зависимость, что добавляли прежде:
1implementation 'com.scalar.maven:scalar:0.1.0' - Добавляем зависимость:
1implementation 'org.springdoc:springdoc-openapi-starter-webmvc-scalar:2.8.13' - Также давайте не забывать, что у нас уже был подключён Swagger. Поэтому нужно вернуть часть от Swagger-а назад дабы вернуть состояние приложения с которым я работал:
1implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13'
123456springdoc:api-url: http://localhost:8080api-docs:path: /v3/api-docsswagger-ui:enabled: true - Запускаем, идём на localhost:8080/docs. И, о, чудо! Заработало!
Только есть проблема. Работает как Swagger, так и Scalar. Вроде решение очевидное. Давайте изменим свойство springdoc.swagger-ui.enabled=false. Делаем. И не работает! Ни Swagger, ни Scalar! С ума сойти! Нафига мне две UI-ки? Лезу в код:
И что мы видим? А видим мы прямую зависимость Scalar-а от Swagger-а (стр. 24-25). Зачем?!
Как позже выяснилось отсюда было два выхода. Убрать зависимость Swagger-а или обновить версию Springdoc-а. Я пошёл по второму пути. Версия 2.8.15 решила эту проблему:
Супер! Анализируем, что получилось!
Основной навык конфигуратора отгадайка
Итак у нас получился весьма симпатичный результат:
Комбобоксик слева вверху. Франкенштейн в конфигурации. Там Springdoc использует конфигурацию Scalar-а. Я промочу о логотипчике и других конфигурационных свойствах, которые собирались отовсюду. Вроде всё устраивает, кроме названий селекторов в комбобоксике. Логично предположить, что это title в настройках скалара. Ок. Делаю:
|
1 2 3 4 5 6 7 8 |
scalar: path: /docs enabled: true sources: - url: /v3/api-docs/admin-group title: Admin API - url: /v3/api-docs/demo-group title: Demo API |
И что? И ничего. Всё без изменений! Мелочь, казалось бы. Но это нужно исправлять! А как понять, что делать? Опять таки, где читать? Нигде. Остаётся код. Лезем в код опять:
И что мы видим? А видим мы, мать вашу, тот же самый контроллер, который я написал вначале. Ничего сложного! Но вот, что касается нужного кода, то его собственно нет. Уходит он в метод org.springdoc.scalar.AbstractScalarController::getDocs. Только вот проблема. Нет такого класса в этом джарнике. Нет даже такого пакета! Хотя пакет от Springdoc!
Запускаю поиск класса AbstractScalarController в Jar Explorer-e по всему локальному репозиторию. И нахожу:
|
1 |
org.springdoc:springdoc-openapi-starter-common:2.8.15 |
Сука! В common-е! В common-е лежит класс отвечающий за отображение Scalar-а! Зачем?!
А в коде мы видим, что source инициируется особым образом в строке 39. title берется не из свойств Scalar-a, а из свойств групп, которые мы определяли ранее в Java коде! Причём, два дурих свойств Scalar-a тупо обнуляются! То есть их невозможно установить от слова никогда! Также рекомендую обратить внимание на строчки 42 и 43. Сейчас нам они не понадобятся, но в дальнешем, еще с ними столкнёмся в другой проблеме. Там тупо меняется композиционная переменная контроллера т.е. синглтона.
Ок. Довавляем названия в группы:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Bean public GroupedOpenApi adminApi() { return GroupedOpenApi.builder() .group("admin-group") .displayName("Admin API") .packagesToScan("com.delfin.demo.scalar.controller.admin") .build(); } @Bean public GroupedOpenApi demoApi() { return GroupedOpenApi.builder() .group("demo-group") .displayName("Demo API") .packagesToScan("com.delfin.demo.scalar.controller.demo") .build(); } |
Смотрим, что получилось:
Супер! Что и требовалось!
- мы мигрировали на Scalar
- вся миграция в виде конфигурации
- эндпоинты разбиты на группы
- также был добавлен логотипчик, убрана консоль разработчика, добавлен title на табку браузера и другие мелочи
Вроде всё готово! Можно в прод!
Деплою в темп для теста и… нихрена не работает!
Страничка грузится, JavaScript загружается, всё подтягивается кроме OpenAPI JSON файла. Ссылка на него: “url”:”http://temp:443/v3/api-docs/demo-group”. Кик видим протокол HTTP, а порт HTTPS. Браузер такое наебалово не любит, а поэтому отказывается загружать.
Пробую как-то это исправить на своей стороне. Документация бесполезна, поэтому сразу лезу в код:
|
1 2 3 4 5 6 7 8 |
private String buildApiDocsUrl(String requestUrl, String apiDocsPath) { String apiDocsUrl = this.scalarProperties.getUrl();// 164 if ("https://registry.scalar.com/@scalar/apis/galaxy?format=json".equals(this.originalScalarUrl)) {// 165 String serverUrl = requestUrl.substring(0, requestUrl.length() - this.scalarProperties.getPath().length());// 166 apiDocsUrl = serverUrl + apiDocsPath;// 167 } return apiDocsUrl;// 169 } |
Что бросается в глаза? Строчки с 3 по 6. Самый главный вопрос зачем? Зачем тянуть в код тестовый URL Scalar-a? Это когда вы строите демку Scalar-a, чтобы посмотреть как работает. Но зачем её протягивать сквозь Java код – непонятно севершенно!
Тем не менее, вроде как, можно в Scalar-е указать один верхний URL типа: /v3/api-docs и всё должно подтянуть (условие не сработает). Делаю. И всё работает! Ура! Но только первый раз при заходе на страничку. Дальше – нифига! А почему? А помните те две строчки выше, на которые я обращал внимание? Вот они то и влияют на это поведение. Первый раз все хорошо, а потом null вместо URL потому как оно перетёрлось в свойствах.
Следующим действием попробовал я последнюю версию org.springdoc:springdoc-openapi-starter-webmvc-scalar. На тот момент 3.0.1. А эта зараза требует SpringBoot4. Я вроде подглядел в код последнего Springdoc Scalar-a и мне показалось что там что-то изменено. Посим я из последних сих заодно обновил наш SpringBoot3 со всеми зависимостями, но всё было по прежнему!
Выть хотелось! Бред какой-то. Никак здесь в моём случае невозможно избежать абсолютной ссылки.
Как выяснилось позже наша аппка была спрятана за проксиком, а эти проксики на каждом энвайрменте были по разному сконфигурированы, на одном, например, был протокол HTTP, а на другом HTTPS. Поэтому пришлось захардкодить в настройках деплоймента соответствующие хедеры:
|
1 2 |
x-forwarded-proto=https x-forwarded-port=443 |
После этого всё заработало.
P.S. Польза от всякоразных ИИ закончилась на кастомном контроллере. В дальнейшем все, что мне удалось попробовать, были абсолютно бесполезны. Один из них до последнего пытался мне вставить свойство логотипчика в конфигурацию Scalar-а. Хотя его там никогда не было и нет до сих пор. Такое…










