OpenAPI Generator
In the previous document we wrote an OpenAPI Specification that defines our API contract. Now we'll use that spec to generate primary adapters automatically, instead of writing them by hand.
The problem with manual primary adapters
Unless you work in microservices, a project usually has more than a few endpoints. As your domain grows, manually crafting primary adapters becomes a monumental task. It comes with a lot of drawbacks:
- Time-consuming: Every endpoint, every model, all by hand. It's a grind.
- Prone to human error: One typo, one missed field, and suddenly your API isn't doing what it's supposed to.
- Compromises documentation and maintainability: The moment your code deviates from your spec, your documentation is a lie, and your maintainability takes a nosedive.
The SIGEM project (a Grails monolith) has 76 controllers and 1102 endpoints.

Files overview
- Java
- Kotlin
- Groovy
Generating code
Let’s save us some problems by using openapi-generator, a library that turns your OpenAPI spec into Spring adapters.
- Java
- Kotlin
- Groovy
id 'org.openapi.generator' version '7.20.0'
implementation 'io.swagger.core.v3:swagger-annotations:2.2.45'
implementation 'org.openapitools:jackson-databind-nullable:0.2.9'
implementation 'org.springframework.boot:spring-boot-starter-validation'
You can find more information about the different configurations in the Documentation for the spring Generator.
id("org.openapi.generator") version "7.20.0"
val swaggerCoreVersion = "2.2.45"
implementation("io.swagger.core.v3:swagger-annotations:$swaggerCoreVersion")
implementation("io.swagger.core.v3:swagger-models:$swaggerCoreVersion")
implementation("org.springframework.boot:spring-boot-starter-validation")
You can find more information about the different configurations in the Documentation for the kotlin-spring Generator.
id 'org.openapi.generator' version '7.20.0'
implementation 'io.swagger.core.v3:swagger-annotations:2.2.45'
implementation 'org.openapitools:jackson-databind-nullable:0.2.9'
implementation 'org.springframework.boot:spring-boot-starter-validation'
While a groovy generator exists, it's a client generator. For our use case (generating server-side code), it doesn't quite fit the bill. That's why we stick with the "spring" generator, which generates .java classes.
inputspec must be pointing to the desired OpenAPI Specification YAML file
(src/main/resources/openapi.yaml).
Now that everything is set up, run the Build Task. When the task finishes, check the build\generated\sources\openapi folder. You’ll find the representation of the OpenAPI Specification (our contract) in classes, ready to be used.
Understand the generated code
- What’s Inside the Generated Code?
- Models: Java (or Kotlin) classes mirroring your OpenAPI schemas (e.g.,
Film). These include validation annotations, serialization logic, and builder patterns. - API Interfaces: Spring
@RestControllerinterfaces (e.g.,FilmApi) that define your endpoints and their method signatures.
- Models: Java (or Kotlin) classes mirroring your OpenAPI schemas (e.g.,
- Why Does It Look So Complicated? The generated code includes:
- Boilerplate for OpenAPI/Spring compatibility (e.g.,
@Validated,@Generatedannotations). - Validation logic (e.g.,
@NotNull,@Size) to enforce your contract. - Serialization/deserialization support (e.g., JSON ↔ Java (or Kotlin) object mapping).
- Boilerplate for OpenAPI/Spring compatibility (e.g.,
- Should I care? No.
- It’s autogenerated: Treat it like a compiled dependency. You use it, not modify it.
- Contract-first philosophy: The code exactly matches your OpenAPI spec. If you need changes, update the YAML file and regenerate.
- Maintenance-free: The generator handles updates, so you avoid manual refactoring.
Use the generated code
- Make the
@RestControllerclass implement the generatedApiinterface:- Java
- Kotlin
- Groovy
- Update the
Mapperinterface so it returnsgenerated.model.Filmmodel instead of the handwritten one:- Java
- Kotlin
- Groovy
java/dev/pollito/spring_java/sakila/film/adapter/in/rest/FilmRestMapper.javapackage dev.pollito.spring_java.sakila.film.adapter.in.rest;import static org.mapstruct.MappingConstants.ComponentModel.SPRING;import dev.pollito.spring_java.sakila.film.domain.model.Film;import org.mapstruct.Mapper;@Mapper(componentModel = SPRING)public interface FilmRestMapper {dev.pollito.spring_java.sakila.generated.model.Film map(Film source);} - Update the
RestControllerAdviceclass so it returns the generatedErrormodel (which modelsProblemDetail) to keep consistency:- Java
- Kotlin
- Groovy
- Update the Domain models so they better represent the use cases:
- Enum interface + utility class pattern:
- Java
- Kotlin
- Groovy
java/dev/pollito/spring_java/common/ValuedEnum.javapackage dev.pollito.spring_java.common;public interface ValuedEnum<T> {T getValue();}kotlin/dev/pollito/spring_kotlin/common/ValuedEnum.ktpackage dev.pollito.spring_kotlin.commoninterface ValuedEnum<T> {fun getValue(): T}groovy/dev/pollito/spring_groovy/common/ValuedEnum.groovypackage dev.pollito.spring_groovy.commoninterface ValuedEnum<T> {T getValue()}- Java
- Kotlin
- Groovy
kotlin/dev/pollito/spring_kotlin/common/util/EnumUtils.ktpackage dev.pollito.spring_kotlin.common.utilimport dev.pollito.spring_kotlin.common.ValuedEnumobject EnumUtils {fun <E> fromValue(enumClass: Class<E>, value: Any): E where E : Enum<E>, E : ValuedEnum<*> {return enumClass.enumConstants.firstOrNull { it.getValue() == value }?: throw IllegalArgumentException("Unknown ${enumClass.simpleName} value: $value")}}- Domain model update:
- Java
- Kotlin
- Groovy
- Delete handwritten
FilmResponse, it is no longer needed.
Build and run the application. Then go to http://localhost:8080/api/films/42
What's the point of generating code?
Setting up this OpenAPI Generator might feel like performing ancient rituals just to get some code out. You might be thinking, "Wasn't this supposed to save me time?" And to that, I say: Yes, you're investing a little pain upfront to avoid a lot of pain later.
The OpenAPI Generator doesn't just spew out code; it is a very strict guardian of your API contract.
- No more manual DTO creation: Forget the mind-numbing task of writing Data Transfer Objects (DTOs) by hand. Your API specification defines them, and the generator builds them perfectly, every single time. This kills off an entire category of typos and inconsistencies.
- Trivial controller implementations: Your Spring
@RestControllerinterfaces, with all their routing annotations, are also generated. This means your controller classes transition from boilerplate factories to minimalist components that simply implement a predefined interface. - Compiler-enforced contract: Because your DTOs and controller interfaces are direct reflections of your OpenAPI spec, the compiler becomes your strictest ally. A missing field, a changed type, an altered endpoint signature, will result in a compile error. It is impossible to break your API contract without immediate feedback.
So, yeah, it's a bit of a setup, but the payoff? Absolutely worth it.