Contract-Driven Development 6: Using a better plugin
Posted on January 16, 2024 • 6 minutes • 1123 words • Other languages: Español
Openapi generator plugin to the rescue.
Check the github repo
This is a continuation of Contract-Driven Development 5: Controller validations aren’t working… Why? .
Everything we’ll do here, you can find in in the github repo.
Spring City Explorer - Backend: Branch feature/cdd-6
Investigation time
In case you haven’t read previous post or don’t remember, back in Contract-Driven Development: Crafting Microservices from the Ground Up I said:
[…] the bank bought this super secret all powerful library that given some yaml + configurations in the build.gradle, on build generates lots of boilerplate, related to things such as controller interfaces […]
Cause the bank is very secretive about it, I don’t have much info. Only the dependency line from the build.gradle and that someone said it was made and mantained by NTT DATA Group .
So after the sour taste that the previous post left in my mouth, I went to investigate a little bit about it. I went to read the generated classes, and one thing caught my attention: all the generated classes start with this comment.
/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.6.0)
*/
I feel so silly that I haven’t read that before. I’m trying to make a cheap clone of an existing tool, obviously I should’ve started by looking at the tool inner files.
So that opened a whole new rabbit hole in generators, which starting point is OpenAPI Generator
Yeeting Swagger Codegen, adding OpenAPI Generator
For this, I started to create the current branch cdd-6 out of cdd-4, and pretend cdd-5 never really happened, except for the improvements in the OAS yaml files.
Kudos to Khanh Nguyen , I’ve used his blog Generate API contract using OpenAPI Generator Maven plugin as a guide. It is featured in the Presentations/Videos/Tutorials/Books section of the OpenAPI Generator github.
Updating dependencies
A lot of dependencies are not needed anymore, a lot of new ones are. OpenAPI Generator relies on:
-
jackson-databind-nullable (org.openapitools): Enables handling of Java 8 Optionals and other nullable types within Jackson, useful for dealing with JSON fields that may be absent.
-
springfox-swagger2 (io.springfox): Integrates Swagger 2 for API documentation into a Spring Boot application. Excludes swagger-annotations from io.swagger.core.v3 to avoid conflicts between different versions of swagger-annotations brought in by other dependencies.
-
swagger-core-jakarta (io.swagger.core.v3): Includes the swagger-core-jakarta library, used for API modeling, which includes annotations and core functionality.
-
spring-boot-starter-validation (org.springframework.boot): It integrates the Hibernate Validator and the Validation API, providing a seamless experience for adding validation capabilities to Spring Boot applications. Ensures compatibility with other Spring Boot 3.x components
- without this one the project will build and run, but the validations will be ignored.
Downgrading to Spring Boot 3.1.7
At the moment of writing this blog, we have to compromise downgrading from Spring Boot 3.2.1 to 3.1.7. But for having jakarta packages working, it is a smart interchange.
---
APPLICATION FAILED TO START
---
Description:
Your project setup is incompatible with our requirements due to following reasons:
- Spring Boot [3.2.1] is not compatible with this Spring Cloud release train
Action:
Consider applying the following actions:
- Change Spring Boot version to one of the following versions [3.0.x, 3.1.x] .
You can find the latest Spring Boot versions here [https://spring.io/projects/spring-boot#learn].
If you want to learn more about the Spring Cloud Release train compatibility, you can visit this page [https://spring.io/projects/spring-cloud#overview] and check the [Release Trains] section.
If you want to disable this check, just set the property [spring.cloud.compatibility-verifier.enabled=false]
Configuring the plugin
Here’s how the plugin code in the pom.xml looks:
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.2.0</version>
<executions>
<execution>
<id>generation from springcityexplorer</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/openapi/springcityexplorer.yaml</inputSpec>
<generatorName>spring</generatorName>
<output>${project.build.directory}/generated-sources/openapi/springcityexplorer/</output>
<apiPackage>dev.pollito.springcityexplorer.api</apiPackage>
<modelPackage>dev.pollito.springcityexplorer.models</modelPackage>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
<useEnumCaseInsensitive>true</useEnumCaseInsensitive>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
Some notes:
- I think leaving the output to just ${project.build.directory}/generated-sources/openapi/ would be good.
- In the future we may consider doing something about the values of apiPackage and modelPackage. It is possible that when we generate more code, classes may start clashing.
- useEnumCaseInsensitive is really useless if we don’t create custom converters and register them in WebMvcConfig. More on that just below.
Weird behaviour about case-sensitives enums in controller overriden methods
Issue
After compiling the project, creating controllers, and making each controller implements their correspoinding autogenerated interface + overriding the needed methods, everything should be working ok. And some unit test even confirm that.
But when running the application, something weird happens with those controllers methods in which some parameter is an autogenerated enum type.
Let’s analyze the /getArticles behaviour:
- If we take a look at the OAS, we see that expects a non required query param “country”. If we try it out, we can get the following total valid cURL
curl -X GET "http://localhost:8080/article?country=ar" -H "accept: application/json"
but when executing, we get the following error
{
"timestamp": "2024-01-17T00:40:29.555+00:00",
"status": 400,
"error": "Bad Request",
"message": "Failed to convert value of type 'java.lang.String' to required type 'dev.pollito.springcityexplorer.models.CountryEnum'; Failed to convert from type [java.lang.String] to type [@io.swagger.v3.oas.annotations.Parameter @jakarta.validation.Valid @org.springframework.web.bind.annotation.RequestParam dev.pollito.springcityexplorer.models.CountryEnum] for value [ar]",
"path": "/article"
}
And this gets weirder when we go to the autogenerated code for CountryEnum and we see the @JsonCreator method is capable of ignore case values.
@JsonCreator
public static CountryEnum fromValue(String value) {
for (CountryEnum b : CountryEnum.values()) {
if (b.value.equalsIgnoreCase(value)) {
return b;
}
}
throw new IllegalArgumentException("Unexpected value '" + value + "'");
}
So how to fix it? We need to create custom converters for every Enum-like generated object being used in a controller, and register them in WebMvcConfig. I haven’t yet found some way to do this automatically.
Creating converters
public class StringToCountryEnumConverter implements Converter<String, CountryEnum> {
@Override
public CountryEnum convert(String source) {
return CountryEnum.fromValue(source);
}
}
public class StringToSortOrderEnumConverter implements Converter<String, SortOrderEnum> {
@Override
public SortOrderEnum convert(String source) {
return SortOrderEnum.fromValue(source);
}
}
Register them
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToCountryEnumConverter());
registry.addConverter(new StringToSortOrderEnumConverter());
}
Misc: Brief intro to Pitest
Totally unrelated what’s been archieved in this post, I wanted to add pitest to the project. As it states in its page :
PIT is a state of the art mutation testing system, providing gold standard test coverage for Java and the jvm
At the moment, just see it like some metrics that says how good are our tests. We are not looking for reaching any particular percentage in particular, but we will maybe look further into it in the future.
Add the following plugin in pom.xml
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.16.3</version>
<executions>
<execution>
<id>pit-report</id>
<phase>test</phase>
<goals>
<goal>mutationCoverage</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.1</version>
</dependency>
</dependencies>
<configuration>
<excludedClasses>
<param>dev.pollito.springcityexplorer.api.*</param>
<param>dev.pollito.springcityexplorer.models.*</param>
</excludedClasses>
</configuration>
</plugin>
and from now, on maven test, it will generate a report that you can find in target/pit-reports/index.html.
Next steps
- Autogenerate the feign client interface.
- Make services interfaces use previous generated interfaces.