golang

Genoas — ещё один генератор OpenAPI 3.1 для Go

  • суббота, 17 октября 2026 г. в 00:00:13
https://habr.com/ru/articles/1095222/

Всем привет!

Меня зовут Егор, и я уже несколько лет пишу на 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

Подмена типа поля

swaggerType, включая композицию: oneOf / anyOf / allOf через перечисление типов

swaggertype — только простая подмена

multipart/form‑data

по 3.1: псевдотип file, contentMediaType, contentEncoding

formData‑параметры в стиле 2.0

Полиморфизм на полях и телах

oneOf / anyOf / allOf, nullable в синтаксисе 3.1

ограниченно

Предсказуемость вывода

табличные unit‑тесты всех парсеров + детерминированный e2e‑гейт (make golden-check)

—

Потребление памяти

лёгкая загрузка пакетов (~300 МБ на больших проектах)

известные проблемы с OOM на больших проектах

Зрелость и экосистема

молодой проект

зрелый: интеграции с gin/echo/fiber, огромное сообщество

Полнота покрытия спецификации

растёт

шире, но в рамках 2.0

x‑расширения (x-...)

пока нет

есть

Типы в комментариях

реальные Go‑типы как есть: []models.Pet, map[string]int, GenericModel[A,B]

своя нотация: {object}, {array} — тип транслируется вручную

Свойства тегов

именованные, в любом порядке: description(...) example(20)

позиционные аргументы: @Param id path int true "Pet ID"

Стили записи свойств

два на выбор: parensStyle и goStyle

один

Content‑Type

mime прямо в строке тега: @Success 200 json ...

отдельные теги @Accept / @Produce

Метаданные API

блок @Info в любом файле проекта

привязка к общему комментарию (обычно в main.go)

Как это выглядит целиком

В репозитории есть директория 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. Документация — здесь.