{"id":20645010,"url":"https://github.com/romanow/openapi-generation","last_synced_at":"2026-05-06T00:07:04.649Z","repository":{"id":159140791,"uuid":"634344710","full_name":"Romanow/openapi-generation","owner":"Romanow","description":"Public report about OpenAPI generator template customization","archived":false,"fork":false,"pushed_at":"2025-03-19T20:08:51.000Z","size":57533,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-19T21:29:23.739Z","etag":null,"topics":["openapi","openapi-codegen","openapi-generator","spring-boot"],"latest_commit_sha":null,"homepage":"https://romanow.github.io/openapi-generation/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Romanow.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-04-29T20:20:33.000Z","updated_at":"2025-03-19T20:08:55.000Z","dependencies_parsed_at":null,"dependency_job_id":"3eee69a8-1a1a-44b1-b760-01223345e9d8","html_url":"https://github.com/Romanow/openapi-generation","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Romanow/openapi-generation","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fopenapi-generation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fopenapi-generation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fopenapi-generation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fopenapi-generation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Romanow","download_url":"https://codeload.github.com/Romanow/openapi-generation/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fopenapi-generation/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32672688,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-05T11:29:49.557Z","status":"ssl_error","status_checked_at":"2026-05-05T11:29:48.587Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["openapi","openapi-codegen","openapi-generator","spring-boot"],"created_at":"2024-11-16T16:18:23.679Z","updated_at":"2026-05-06T00:07:04.642Z","avatar_url":"https://github.com/Romanow.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![CI](https://github.com/Romanow/openapi-generation/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/Romanow/openapi-generation/actions/workflows/build.yml)\n[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)\n[![Servers](https://img.shields.io/docker/pulls/romanowalex/servers?logo=docker)](https://hub.docker.com/r/romanowalex/servers)\n[![License](https://img.shields.io/github/license/Romanow/openapi-generation)](https://github.com/Romanow/openapi-generation/blob/main/LICENSE)\n\n# Contract First или страх и ненависть в королевстве CodeGen\n\n## SQA Days 34\n\n### Аннотация\n\nЧтобы end-2-end тесты были максимально полезными, их нужно начинать писать параллельно с разработкой самой задачи. Но\nкак это сделать прозрачно, ведь API может поменяться как во время разработки, так и в процессе доработок по другим\nзадачам. Рассмотрим подход Contract First на базе OpenAPI как средство поддержания актуальности e2e тестов.\n\n## План\n\n1. Как быстро начать автоматизацию задачи и следить за изменениями в автоматизированном режиме?\n2. Что такое контракт? OpenAPI.\n3. Рассматриваем что умеет проект [OpenAPI Generator](https://openapi-generator.tech/)\n4. Берем OpenAPI Generator и по контракту генерируем модели и клиента `RestAssured`.\n5. Убираем лишние файлы.\n6. Используем свои шаблоны генерации кода по OpenAPI.\n7. А что делать, если реализация еще недоступна? С помощью Postman и OpenAPI создаем Mock Server и делаем вызовы.\n8. Вместо выводов: как максимально быстро реагировать в тестах на изменения в коде?\n\n### Подготовка\n\nДля выполнения мастер-класса нужно:\n\n1. Java 17: [установка](https://www.oracle.com/java/technologies/downloads/#java17).\n2. Docker: [установка](https://docs.docker.com/engine/install/).\n3. OpenAPI generator: [установка](https://openapi-generator.tech/docs/installation/).\n4. Postman: [установка](https://www.postman.com/downloads/).\n\nКод проекта: [openapi-generation](https://github.com/Romanow/openapi-generation).\n\n```shell\n$ git clone git@github.com:Romanow/openapi-generation.git\n$ ./gradlew clean build\n\n```\n\nСервис `servers` развернут по адресу https://servers.romanow-alex.ru](https://servers.romanow-alex.ru) (на время\nдоклада). Либо можно запустить локально:\n\n```shell\n$ docker compose up -d --wait\n```\n\n## Импульс\n\n### Аннотация\n\nЧасто для решения одной бизнес задачи требуется коммуникация и совместная разработка нескольких команд. В микросервисной\nархитектуре задача упрощается, ведь сервисы имеют разную кодовую базу и взаимодействуют друг с другом по API. А значит\nтребуется лишь договориться об общем API в начале разработки. Но так ли все просто и как автоматизировать этот процесс\nрассмотрим в нашем докладе.\n\n### План доклада\n\n1. Коммуникация между командами разработки при совместной работе над одной задачей.\n2. Что такое контракт? OpenAPI.\n3. Contract First vs. Code First. Плюсы и минусы подходов.\n4. Рассматриваем что умеет проект [OpenAPI Generator](https://openapi-generator.tech/).\n5. Генерируем код по контракту на клиенте и сервере, разбираем что получилось.\n6. Убираем лишние файлы.\n7. Используем свои шаблоны генерации кода по OpenAPI.\n8. Вместо выводов: как поддержать баланс между чувством прекрасного и сгенерированным кодом?\n\n## Доклад\n\n### Коммуникация между командами разработки при совместной работе над одной задачей\n\nВ мире микросервисов практически любая задача требует взаимодействия нескольких сервисов, а значит для ее выполнения\nтребуется параллельно вести разработку в нескольких командах. Для этого нужна договоренность между командами, по\nкакому API (Application Programming Interface) ону будут взаимодействовать. Ведь если _Сервис А_ вызывает _Сервис B_, то\nкоманда, ответственная за _Сервис A_ не может ждать, пока команда, ответственная за _Сервис B_ возьмет задачу в работу и\nчто-то напишет. Следовательно, нам надо заранее (до начала разработки) описать некоторый контракт, который будет\nреализовываться в _Сервисе B_, а _Сервис А_ может начать его использовать на заглушках (например, с помощью WireMock или\nPostman Mock Server).\n\n![Command Communication](images/Command%20Communication.png)\n\nКак контролировать корректность данных, которые будут в заглушках, мы не будем рассматривать, скажу лишь, что стоит\nсмотреть в сторону [контрактных тестов](https://github.com/Romanow/scc-contracts)\n([Использование Spring Cloud Contract как альтернатива для интеграционных тестов](https://www.youtube.com/watch?v=iavb9QiD60Y)).\n\n### Как быстро начать автоматизацию задачи и следить за изменениями в автоматизированном режиме?\n\nКогда вашу систему разрабатывают несколько команд, сложно следить за актуальностью моделей и API (кто-то что-то поменял\nи забыл вас известить об этом). Хотелось бы отлавливать ситуацию, что что-то поменялось, как можно раньше, например\nна этапе компиляции.\n\nТак же, когда появляется новый сервис, для старта автоматизации приходится писать много boilerplate кода.\n\n### Что такое контракт? OpenAPI\n\nКонтракт – это соглашение о том, как будет выглядеть наше API и какие параметры оно будет принимать.\n\nВ рамках доклада будем рассматривать синхронную коммуникацию с помощью REST, следовательно, мы строим наше API на базе\nпротокола HTTP. Самым распространенным способом описания контракта для REST сервисов\nявляется [OpenAPI](https://spec.openapis.org/oas/latest.html).\n\nСпецификация OpenAPI определяет стандарт независимого от языка описания API, который позволяет людям и машинам понимать\nвозможности службы без доступа к исходному коду, документации или путем перехвата сетевого трафика. По сути, OpenAPI —\nэто описание методов API, для которых описываются заголовки, входные и выходные параметры.\n\n![Contract Communication](images/Contract%20Communication.png)\n\n### Contract First vs. Code First. Плюсы и минусы подхода\n\nИтак, мы описали и согласовали контракт между командами, теперь можем приступать к реализации.\n\nНо как поддерживать консистентность контракта с тем, что реально реализовано в коде? Можно при каждом релизе сравнивать\nкод с контрактом, но все равно в каких-то мелочах они могут разойтись, а случае ошибки может быть сложно установить ее\nпричину. Например: метод ищет сущность по ID (`/api/v1/users/1`), в ответ может вернуться `404 Not Found`, если сущность\nне найдена. Но `404 Not Found` может вернуться и в случае, если такого метода больше нет в коде. И для разбора в этой\nпроблеме потребуется время.\n\nЕдинственный _надежный_ способ поддерживать в консистентном состоянии два источника правды – это получать одно из\nдругого. Рассмотрим два подхода:\n\n* Code First – мы сначала пишем код, помечаем методы специальными аннотациями `@Tag`, `@Opearation` и т.п., а по этим\n  аннотациям автоматически генерируется документация. В итоге получаем OpenAPI, полностью соответствующий коду. Но в\n  этом подходе есть 3 проблемы:\n    * в коде не все можно описать (доп. параметры, валидаторы и т.п.), следовательно, мы можем получить невалидный или\n      неполный OpenAPI;\n    * требуется время для написания кода, следовательно, другие команды вынуждены нас ждать;\n    * есть проблемы при генерации методов со сходной сигнатурой (`/api/v1/users` и `/api/v1/users?login={1}`) – они\n      сливаются в один метод.\n* Contract First – у нас есть согласованный со всеми сторонами контакт, по нему мы генерируем код. Сгенерированный код\n  рабочий, но проблема в том, что он выглядит очень плохо и работать с ним зачастую неудобно. Плюс в команде есть Code\n  Style, соглашения о названиях классов и т.п., а значит нам хочется, чтобы _весь_ код в проекте удовлетворял этим\n  критериям.\n\n### Рассматриваем что умеет проект [OpenAPI Generator](https://openapi-generator.tech/)\n\nПогрузимся глубже в Contract First и посмотрим что можно улучшить. Для генерации кода возьмем самое распространенное\nрешение – проект [OpenAPI Generator](https://openapi-generator.tech/) и посмотрим что оно умеет:\n\n* генерация клиентского и серверного кода для всем популярных языков и фреймворков (Java, Kotlin, Go, C#, JavaScript\n  и [т.п.](https://openapi-generator.tech/docs/generators));\n* валидация OpenAPI;\n* возможность **кастомизации** шаблонов.\n\n```shell\n# установка OpenAPI Generator\n$ npm install @openapitools/openapi-generator-cli -g\n\n```\n\n### Генерируем код по контракту на клиенте и сервере, разбираем что получилось\n\nРассматривать будем пример OpenAPI [servers.yml](openapi/servers.yml), сгенерируем клиентский код с помощью\nopenapi-generator:\n\n```shell\n$ openapi-generator generate \\\n    -g kotlin \\\n    --api-package ru.romanow.openapi.client.rest \\\n    --model-package ru.romanow.openapi.client.models \\\n    --additional-properties=dateLibrary=java8,serializationLibrary=jackson,enumPropertyNaming=UPPERCASE \\\n    -o client/build/generated \\\n    -i openapi/servers.yml\n```\n\nВ результате получаем целый проект:\n\n![Generated client](images/Generated%20Client%20Start.png)\n\nПосмотрим на\nфайл [ServerResponse](client/build/generated/src/main/kotlin/ru/romanow/openapi/client/models/ServerResponse.kt):\n\n```kotlin\n/**\n * Please note:\n * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * Do not edit this file manually.\n */\n@file:Suppress(\"ArrayInDataClass\", \"EnumEntryName\", \"RemoveRedundantQualifierName\", \"UnusedImport\")\npackage ru.romanow.openapi.client.models\n\nimport ru.romanow.openapi.client.models.StateInfo\nimport com.fasterxml.jackson.annotation.JsonProperty\n\ndata class ServerResponse(\n    @field:JsonProperty(\"id\") val id: kotlin.Int,\n    @field:JsonProperty(\"purpose\") val purpose: ServerResponse.Purpose,\n    @field:JsonProperty(\"latency\") val latency: kotlin.Int,\n    @field:JsonProperty(\"bandwidth\") val bandwidth: kotlin.Int,\n    @field:JsonProperty(\"state\") val state: StateInfo\n)\n```\n\nКод получился неплохой, но все равно много лишнего, например аннотация `@JsonProperty` или enum как inner class.\n\nЧтобы не держать огромную команду запуска перенесем основные параметры в [config.yml](openapi/client/config.yml).\n\n#### Лишние файлы\n\nПолученный шаблон кода оформлен как проект, т.е. его можно собрать и использовать как модуль или опубликовать в\nрепозиторий. Но наша задача сгенерировать код моделей, а все остальное не нужно\n\nВ сгенерированном коде мы получаем большое количество лишних файлов:\n\n* `build.gradle`, `settings.gradle`, `gradlew`, `gradlew.bat`, `gradle/`;\n* `README.md`, `docs/`\n* файлы в\n  пакете [/org/openapitools/client/infrastructure/](client/build/generated/src/main/kotlin/org/openapitools/client/infrastructure)\n\nОпишем эти исключения в файле [.openapi-generator-ignore](openapi/client/.openapi-generator-ignore).\n\n```shell\n$ openapi-generator generate \\\n  -g kotlin \\\n  --config openapi/client/config.yml \\\n  --ignore-file-override openapi/client/.openapi-generator-ignore \\\n  -o client/build/generated \\\n  -i openapi/servers.yml\n\n```\n\nНа этот раз мы убрали все лишние файлы:\n\n![Generated client](images/Generated%20Client%20With%20Ignored%20Files.png)\n\n### Используем свои шаблоны генерации кода по OpenAPI\n\n#### Структура шаблона\n\nМы избавились от лишних файлов, теперь займемся кастомизацией шаблона. В качестве движка шаблонизации\nиспользуется [mustache](https://mustache.github.io/).\n\nВ OpenAPI generator можно переопределить часть шаблона генерации, при этом остальные файлы оставить без изменений.\n\nДля начала надо выгрузить шаблон:\n\n```shell\n$ openapi-generator author template -g kotlin --library jvm-ktor -o openapi/templates\n```\n\nЗаходим в [openapi/client/templates](openapi/client/templates) и видим большое количество шаблонов:\n\n![OpenAPI codegen templates](images/OpenAPI%20codegen%20templates.png)\n\nФайлов много, но по сути есть 5 входных типов файлов, а остальные просто являются частью других шаблонов:\n\n* [API](openapi/templates/libraries/jvm-ktor/api.mustache) – клиент или сервер;\n* [APIDocs](openapi/templates/api_doc.mustache) – markdown описание API;\n* [Model](openapi/templates/model.mustache) – модели;\n* [ModelDocs](openapi/templates/model_doc.mustache) – markdown описание моделей;\n* SupportingFiles – дополнительные файлы.\n\nНам нужна кастомизация моделей, поэтому удаляем лишнее и оставляем только три файла:\n\n* [common model template](openapi/client/templates/model.mustache) – общий шаблон модели;\n* [data class](openapi/client/templates/data_class.mustache) – `data class`;\n* [enum](openapi/client/templates/enum_class.mustache) – `enum`.\n\nДля примера рассмотрим шаблон [`model`](openapi/client/templates/model.mustache):\n\n```\n{{\u003elicenseInfo}}\npackage {{modelPackage}}\n\n{{#imports}}\nimport {{import}}\n{{/imports}}\n{{#models}}{{#model}}\n{{#isEnum}}{{\u003eenum_class}}{{/isEnum}}{{^isEnum}}{{\u003edata_class}}{{/isEnum}}\n{{/model}}{{/models}}\n\n```\n\n* `{{\u003e...}}` – подключение шаблона;\n* `{{...}}` – обращение к значению переменной;\n* `{{#...}} .... {{\\...}}` – обращение к переменной (так же используется для обхода списка).\n\nТ.е. в этом примере подключается шаблон [`licenseInfo.mustache`](openapi/client/templates/licenseInfo.mustache), а\nпотом, если модель – enum, то подключаем шаблон [`enum_class.mustache`](openapi/client/templates/enum_class.mustache),\nиначе [`data_class.mustache`](openapi/client/templates/data_class.mustache).\n\n```\ndata class {{classname}} (\n{{#vars}}\n    var {{name}}: {{#isArray}}{{#isList}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}{{/isList}}{{^isList}}Array{{/isList}}\u003c{{^items.isEnum}}{{^items.isPrimitiveType}}{{/items.isPrimitiveType}}{{{items.dataType}}}{{/items.isEnum}}{{#items.isEnum}}{{{nameInCamelCase}}}{{/items.isEnum}}\u003e{{/isArray}}{{^isEnum}}{{^isArray}}{{{dataType}}}{{/isArray}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{{nameInCamelCase}}}{{/isArray}}{{/isEnum}}? = null{{^-last}},{{/-last}}\n{{/vars}}\n)\n```\n\nOpenAPI generator разбирает OpenAPI и собираем объект, который передает в шаблонизатор:\n\n```json\n{\n    \"importPath\": \"ru.romanow.openapi.client.models.CreateServerRequest\",\n    \"model\": {\n        \"name\": \"CreateServerRequest\",\n        \"classname\": \"CreateServerRequest\",\n        \"isPrimitiveType\": false,\n        \"vars\": [\n            {\n                \"openApiType\": \"string\",\n                \"dataType\": \"kotlin.String\",\n                \"name\": \"purpose\",\n                \"baseType\": \"kotlin.String\",\n                \"required\": true,\n                \"deprecated\": false,\n                \"isPrimitiveType\": true,\n                \"isContainer\": false,\n                \"isString\": true,\n                \"isNumeric\": false,\n                \"isInteger\": false,\n                \"isShort\": false,\n                \"isLong\": false,\n                \"isUnboundedInteger\": false,\n                \"isNumber\": false,\n                \"isFloat\": false,\n                \"isDouble\": false,\n                \"isDecimal\": false,\n                \"isByteArray\": false,\n                \"isBinary\": false,\n                \"isFile\": false,\n                \"isBoolean\": false,\n                \"isDate\": false,\n                \"isDateTime\": false,\n                \"isUuid\": false,\n                \"isEmail\": false,\n                \"isPassword\": false,\n                \"isNull\": false,\n                \"isVoid\": false,\n                \"isFreeFormObject\": false,\n                \"isAnyType\": false,\n                \"isArray\": false,\n                \"isMap\": false,\n                \"isEnum\": false,\n                \"isInnerEnum\": false,\n                \"isEnumRef\": false,\n                \"isReadOnly\": false,\n                \"isWriteOnly\": false,\n                \"isNullable\": false,\n                \"vars\": [],\n                \"requiredVars\": [],\n                \"hasValidation\": false,\n                \"isInherited\": false,\n                \"nameInCamelCase\": \"Purpose\",\n                \"nameInSnakeCase\": \"PURPOSE\",\n                \"datatype\": \"kotlin.String\",\n                \"hasItems\": false,\n                \"isEnumOrRef\": false\n            }\n        ]\n    }\n}\n```\n\nВ [config.yml](openapi/client/config.yml) указываем измененный шаблон и при запуске OpenAPI Generator передаем папку с\nшаблонами:\n\n```yaml\nfiles:\n    model.mustache:\n        templateType: Model\n        destinationFilename: .kt\n```\n\n![Custom Template](images/Custom%20template.png)\n\n```shell\n$ openapi-generator generate \\\n  -g kotlin \\\n  --config openapi/client/config.yml \\\n  --ignore-file-override openapi/client/.openapi-generator-ignore \\\n  --template-dir openapi/client/templates \\\n  -o client/build/generated \\\n  -i openapi/servers.yml\n```\n\n![Customized Classes Result](images/Cusomized%20classes%20result.png)\n\nЕсли нам не требуется модифицировать шаблон (например\n[ApiController](server/build/generated/src/main/kotlin/ru/romanow/openapi/server/web/ApiController.kt)\nв [server](/openapi/server/config.yml)), то мы просто его не меняем и он создается на основе базовых шаблонов.\n\nЕсли требуется убрать сгенерированные файлы, то описываем их\nв [\\.openapi-generator-ignore](openapi/client/.openapi-generator-ignore).\n\n### Вместо выводов: как поддержать баланс между чувством прекрасного и сгенерированным кодом?\n\n1. Если разработка идет в одной команде, то возможно Code First вам подойдет, т.к. для него не требуется никаких\n   особенных настроек.\n2. Если все-таки вы работаете с Contract First, то нужно смотреть в сторону codegen: без этого реализация быстро\n   разойдется с контрактом.\n3. OpenAPI generator позволяет кастомизировать шаблоны, следовательно, вы можете сгенерировать код, удовлетворяющий Code\n   Style и вашему чувству прекрасного.\n\n### Вместо выводов: как максимально быстро реагировать в тестах на изменения в коде?\n\n1. Возможность генерировать модели и клиента по OpenAPI позволяет вам быстрее реагировать на изменения в реализации.\n2. OpenAPI generator сгенерирует за вас весь boilerplate, вам же останется лишь описать сценарии.\n3. OpenAPI generator позволяет кастомизировать шаблоны, следовательно, вы можете сгенерировать код, удовлетворяющий Code\n   Style и вашему чувству прекрасного.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fromanow%2Fopenapi-generation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fromanow%2Fopenapi-generation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fromanow%2Fopenapi-generation/lists"}