Genoas — ещё один генератор OpenAPI 3.1 для Go
Сегодня я хотел бы представить свой небольшой проект по генерации свагера для Go‑проектов.
С чего всё началось
Когда я работал в банке, у нас были жёсткие требования: API бэкенда должно быть досконально описано. Паттерны для строк, максимальная/минимальная длина и так далее. Но главное — спецификация должна была быть версии не ниже 3.0.
К сожалению, подходящих библиотек на тот момент было не то чтобы много. Я уже не говорю о том, подходили ли они нам вообще. Конечно, был swaggo/swag, но поддержка 3.0 тогда была только в зачатке.
Мы выбрали библиотеку go‑swagger3, которая на тот момент уже долгое время не обновлялась, но в целом позволяла быстро залатать дыру с документацией API. И всё бы так и оставалось, но в какой‑то момент количество ручек и описаний к ним начало ломать ее: пропадали целые куски спеки, точечно терялись свойства полей и значения. Всё чаще приходилось проверять, что именно изменилось в свагере после генерации, — или вовсе править его руками. Плюс она не поддерживала некоторые вещи — например, ответ в виде строки.
К тому же, что в swag, что в go‑swagger3, комментарии к ручкам, лично на мой взгляд — перегружены.
Тогда я решил взяться за её актуализацию и упрощение, но быстро понял, что дело это гиблое. Так я начал потихоньку пилить свою реализацию.
Что получилось
Если вы пытались разобраться в исходниках swag — думаю, согласитесь на сколько это трудно и долго.
В своём проекте я постарался упростить конструкции комментариев, отказаться от ручного парсинга типов и сделать при этом всё максимально просто в плане кода.
Для этого пришлось разбить всё на отдельные парсеры и выстроить из них конвейер. Также мне хотелось, чтобы проект можно было расширять или заменять какие‑то его части, если пользователю вдруг не понравится, как парсится, например, тело запроса из комментария.
Вот небольшой пример того, как удалось упростить конструкции комментариев, сохранив читаемость:
P. S. Здесь и далее я буду сравнивать со swag как с эталоном.
// swag:
// @Summary Список питомцев
// @Param limit query int false "максимум записей"
// @Success 200 {array} models.Pet "список питомцев"
// @Produce json
// @Router /pets [get]
// genoas:
// @Title Список питомцев
// @Query limit int example(20) description(максимум записей)
// @Success 200 json []models.Pet description(список питомцев)
// @Route /pets [get]Еще пример: заголовки ответа.
В swag они описываются так:
// @Header 200 {string} Token "token"В genoas это выглядит так:
// @Header.Response 200 X-Request-Id string example(3fa85f64) description(идентификатор запроса)Обратите внимание: позиционные свойства заменены именованными. Лично я постоянно путался в том, что за чем должно идти, поэтому решил, что строку проще разбирать, зная имена свойств, чем парсить позиции регулярками, как это делает swag, — пусть это и немного многословнее.
Сами свойства можно писать в двух стилях — на вкус:
// parensStyle (по умолчанию)
// @Query limit int minLength(1) example(20) description(максимум записей)
// goStyle — как теги полей структур в Go
// @Query limit int minLength:"1" example:"20" description:"максимум записей"Стиль выбирается флагом --comment-style.
Отдельно про то, как это устроено внутри.
И swag, и go‑swagger3 разбирают свойства регулярными выражениями. Я пошел другим путём: адаптировал reflect.StructTag из стандартной библиотеки, научив его разделять свойства табуляцией и экранировать символы в скобочном стиле. Вместо хрупких регексов — простой сканер в духе stdlib: он не сломается о значение с пробелами или кавычками внутри. Честных бенчмарков не гонял, но линейный проход по строке априори не дороже регексов, а по надёжности на граничных случаях — заметно предсказуемее.
Один из пунктов, который я хотел закрыть, — это документация. Честно, это моя отдельная боль: когда у библиотеки нет подробной документации, приходится додумывать, как использовать ту или иную возможность. Поэтому у genoas стараюсь вести подробную документацию с примерами на каждый тег и каждый тип.
Не буду подробно разбирать каждый пункт — предлагаю краткое сравнение со swag:
genoas | swag | |
Версия спецификации | OpenAPI 3.1+ — основная и единственная цель | OpenAPI 2.0; 3.1 — экспериментальный флаг |
Дженерики | вложенные и кросс‑пакетные; тип поля вычисляется в скоупе объявления модели | поддерживаются, включая вложенные; на кросс‑пакетных кейсах есть открытые issue |
Подмена типа поля |
|
|
multipart/form‑data | по 3.1: псевдотип |
|
Полиморфизм на полях и телах |
| ограниченно |
Предсказуемость вывода | табличные unit‑тесты всех парсеров + детерминированный e2e‑гейт ( | — |
Потребление памяти | лёгкая загрузка пакетов (~300 МБ на больших проектах) | известные проблемы с OOM на больших проектах |
Зрелость и экосистема | молодой проект | зрелый: интеграции с gin/echo/fiber, огромное сообщество |
Полнота покрытия спецификации | растёт | шире, но в рамках 2.0 |
x‑расширения ( | пока нет | есть |
Типы в комментариях | реальные Go‑типы как есть: | своя нотация: |
Свойства тегов | именованные, в любом порядке: | позиционные аргументы: |
Стили записи свойств | два на выбор: | один |
Content‑Type | mime прямо в строке тега: | отдельные теги |
Метаданные API | блок | привязка к общему комментарию (обычно в |
Как это выглядит целиком
В репозитории есть директория for_test — комментариями описано большинство возможностей genoas: query, path и header‑параметры, тела запросов и ответов, массивы, мапы, дженерики, полиморфизм, загрузка файлов.
А рядом лежит oas_golden.yml — «золотая» спецификация, написанная по этим примерам. По ней можно посмотреть, какой YAML получается на выходе, не устанавливая инструмент.
Заодно это e2e‑тест: make golden-check прогоняет генератор по for_test и побайтово сравнивает результат с эталоном. Любое изменение в парсерах, незаметно поменявшее вывод, сразу всплывёт в диффе.
Чего ещё нет
— discriminator для полиморфных схем;
— example / examples на уровне media type и параметров (именованные примеры);
— security на уровне операции (@Security на ручку);
— encoding для multipart/form‑data (content‑type отдельных частей формы);
— application/x-www-form-urlencoded для тела запроса;
— отдельные возможности query‑параметров: allowEmptyValue, allowReserved, массивы структур, content‑сериализация;
— path‑параметры типов boolean и массивов (сейчас — числа и строки).
— @Cookie‑параметры, variables в серверах;
— dependentRequired, patternProperties, not, callbacks, xml‑метаданные;
— webhooks, links, externalDocs, jsonSchemaDialect, components.pathItems;
— HTTP‑метод trace, prefixItems, contains, if/then/else;
— x‑расширения (x-...), drop‑in совместимость с популярными тегами swag (@Accept/@Produce).
Допускаю, что список неполный.
Заключение
Написание кода заняло довольно длительное время с учётом основной работы(с февраля по сегодня). Да и для коллег из банка это стало неактуально: как я позже узнал, там стали работать от обратного — сначала руками свагер, потом по нему бэкенд. В какой‑то момент у меня и вовсе закончилась мотивация, но я решил, что доведу дело до определённой стадии готовности.
Также буду благодарен за обратную связь, и если проект окажется востребованным, то буду рад продолжить над ним работу. Согласитесь, не всегда приятно что‑то делать просто в стол =)
Ссылка на проект — pkg.go.dev и GitLab. Документация — здесь.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.